agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v28 07/11] Add DISTINCT support for IVM 383+ messages / 2 participants [nested] [flat]
* [PATCH v28 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 1 + 8 files changed, 317 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 415f110516..076f35ee6b 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -53,6 +53,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "storage/smgr.h" #include "tcop/tcopprot.h" @@ -309,6 +310,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -413,6 +417,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -536,7 +583,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -940,10 +988,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1090,12 +1134,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1122,41 +1172,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index a5168c9f09..480c3a00ed 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -39,6 +39,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1090,6 +1091,45 @@ DefineIndex(Oid relationId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index fd9d0d99ae..6d8382180a 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -152,11 +152,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -271,6 +275,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -331,8 +336,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -512,8 +522,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1513,6 +1523,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1525,7 +1542,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -1998,7 +2016,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2074,7 +2092,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2096,7 +2119,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2109,6 +2136,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2158,6 +2230,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s)" /* insert a new tuple if this doesn't existw */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 4d49d70c33..25ff2a23db 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/policy.h" #include "commands/sequence.h" @@ -3548,6 +3549,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 41d60494b9..f516ac91e2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1501,6 +1502,7 @@ addRangeTableEntry(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1586,6 +1588,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2757,7 +2760,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3025,7 +3028,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3042,7 +3045,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3055,6 +3058,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3217,6 +3223,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index e36fc72e1e..f6dc7ba202 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -621,7 +621,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 09a64fa2e5..76a7873ebf 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0ca298f5a1..43c4ed49f1 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1074,6 +1074,7 @@ typedef struct RangeTblEntry int rellockmode; /* lock level that query requires on the rel */ struct TableSampleClause *tablesample; /* sampling info, or NULL */ Index perminfoindex; + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Thu__1_Jun_2023_23_59_09_+0900_/G5+8nG46.f1T42K Content-Type: text/x-diff; name="v28-0008-Add-aggregates-support-in-IVM.patch" Content-Disposition: attachment; filename="v28-0008-Add-aggregates-support-in-IVM.patch" Content-Transfer-Encoding: quoted-printable ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v37 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 99bc709959a..ee65292deb0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3923,6 +3924,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 43460e4a5a5..dc048c21bed 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1512,6 +1513,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1593,6 +1595,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2935,7 +2938,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3211,7 +3214,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3229,7 +3232,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3242,6 +3245,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3406,6 +3412,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..59e689a1fac 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1207,6 +1207,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Fri__29_May_2026_23_14_17_+0900_Te0o73X2VqYK57Gd Content-Type: text/x-diff; name="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v37-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v38 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 +++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 145 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 319 insertions(+), 43 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a499688b79d..cd8db0059f9 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -55,6 +55,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -307,6 +308,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -416,6 +420,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -539,7 +586,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -943,10 +991,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1108,12 +1152,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1140,41 +1190,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 9ab74c8df0a..1f95e6b229f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1179,6 +1180,45 @@ DefineIndex(ParseState *pstate, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index eaf39f80cd3..a2746ca9265 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -193,11 +193,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -346,6 +350,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, QueryCompletion *qc) { Relation matviewRel; + Query *viewQuery; Query *dataQuery; Oid tableSpace; Oid relowner; @@ -394,7 +399,13 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, errmsg("%s options %s and %s cannot be used together", "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + + /* For IMMV, we need to rewrite matview query */ + if (!skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -1688,6 +1699,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1700,7 +1718,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2238,7 +2257,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2314,7 +2333,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2336,7 +2360,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2349,6 +2377,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2398,6 +2471,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 920b62d254d..f62765f0a89 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -60,6 +60,7 @@ #include "catalog/toasting.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/matview.h" @@ -3930,6 +3931,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 953c5797c5d..0f228eca259 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -516,6 +516,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index b6b2ce6c792..dba4d8ff2de 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -365,6 +365,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index ced210cd206..5392e9f15b2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -33,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -96,7 +97,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1544,6 +1545,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1625,6 +1627,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2967,7 +2970,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, returning_type, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3243,7 +3246,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, returning_type, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3261,7 +3264,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, VarReturningType returning_type, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3274,6 +3277,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3438,6 +3444,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6a223fbeaa4..74ac631299c 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index c286ebcd70e..bfd0249b10d 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4133c404a6b..cbcef860061 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.43.0 --Multipart=_Wed__1_Jul_2026_00_04_01_+0900_OVSy2WWK_9aByzDJ Content-Type: text/x-diff; name="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v38-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v34 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 320 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a424abbd32..7138dd59ce 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -50,6 +50,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -305,6 +306,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -409,6 +413,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -532,7 +579,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -936,10 +984,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1086,12 +1130,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1118,41 +1168,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 2caab88aa5..462c15f9c8 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -40,6 +40,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1120,6 +1121,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 9629e0a47c..bd7d65888d 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -148,11 +148,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -267,6 +271,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -329,8 +334,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -510,8 +520,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1535,6 +1545,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1547,7 +1564,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2020,7 +2038,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2096,7 +2114,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2118,7 +2141,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2131,6 +2158,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2180,6 +2252,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index dbfe0d6b1c..07382b9cc0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -57,6 +57,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/sequence.h" #include "commands/tablecmds.h" @@ -3696,6 +3697,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 3337b77ae6..c191f70a6f 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -510,6 +510,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index c4d01a441a..ffcab8cda2 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -361,6 +361,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 2f64eaf0e3..a39358f125 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1503,6 +1504,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1588,6 +1590,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2757,7 +2760,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3025,7 +3028,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3042,7 +3045,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3055,6 +3058,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3217,6 +3223,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6cc9a8d8bf..5d22dbcfcf 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 396ad1bb4c..6b47e66bfd 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 85a62b538e..1366946bb4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1106,6 +1106,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.34.1 --Multipart=_Thu__11_Jul_2024_13_23_57_+0900_hKsU2G3a3BgS2FFs Content-Type: text/x-diff; name="v34-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v34-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v33 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 320 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index a424abbd32..7138dd59ce 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -50,6 +50,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -305,6 +306,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -409,6 +413,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -532,7 +579,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -936,10 +984,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1086,12 +1130,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1118,41 +1168,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 2caab88aa5..462c15f9c8 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -40,6 +40,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1120,6 +1121,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 1061c37b2c..f2e8aa02a3 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -148,11 +148,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -267,6 +271,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -329,8 +334,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -510,8 +520,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1533,6 +1543,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1545,7 +1562,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2018,7 +2036,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2094,7 +2112,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2116,7 +2139,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2129,6 +2156,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2178,6 +2250,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 9e9dc5c2c1..ddb0732542 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -57,6 +57,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/sequence.h" #include "commands/tablecmds.h" @@ -3696,6 +3697,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 3337b77ae6..c191f70a6f 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -510,6 +510,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index c4d01a441a..ffcab8cda2 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -361,6 +361,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 2f64eaf0e3..a39358f125 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1503,6 +1504,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1588,6 +1590,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2757,7 +2760,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3025,7 +3028,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3042,7 +3045,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3055,6 +3058,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3217,6 +3223,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6cc9a8d8bf..5d22dbcfcf 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 396ad1bb4c..6b47e66bfd 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 85a62b538e..1366946bb4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1106,6 +1106,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Tue__2_Jul_2024_17_03_11_+0900_6OrVBZxOB4o_k6A1 Content-Type: text/x-diff; name="v33-0006-Add-Incremental-View-Maintenance-support.patch" Content-Disposition: attachment; filename="v33-0006-Add-Incremental-View-Maintenance-support.patch" Content-Transfer-Encoding: 7bit ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v30 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 1 + 8 files changed, 317 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 08262100ea..cce44278fa 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -53,6 +53,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "storage/smgr.h" #include "tcop/tcopprot.h" @@ -309,6 +310,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -413,6 +417,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -536,7 +583,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -940,10 +988,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1090,12 +1134,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1122,41 +1172,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 7a87626f5f..d661fbbb48 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -39,6 +39,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1129,6 +1130,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index a821992a37..dbcbc79fff 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -152,11 +152,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -271,6 +275,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -330,8 +335,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -511,8 +521,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1534,6 +1544,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1546,7 +1563,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2019,7 +2037,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2095,7 +2113,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2117,7 +2140,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2130,6 +2157,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2179,6 +2251,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f798794556..02a688acb7 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/policy.h" #include "commands/sequence.h" @@ -3784,6 +3785,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 34a0ec5901..d5b45c587d 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1502,6 +1503,7 @@ addRangeTableEntry(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1587,6 +1589,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2758,7 +2761,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3026,7 +3029,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3043,7 +3046,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3056,6 +3059,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3218,6 +3224,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index b449244a53..b9d161d6a3 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -621,7 +621,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 396ad1bb4c..6b47e66bfd 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d60e148ff2..fa3a95897c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1074,6 +1074,7 @@ typedef struct RangeTblEntry int rellockmode; /* lock level that query requires on the rel */ struct TableSampleClause *tablesample; /* sampling info, or NULL */ Index perminfoindex; + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Mon__4_Mar_2024_11_58_46_+0900_UaponF/qQhQrVCFt Content-Type: text/x-diff; name="v30-0008-Add-aggregates-support-in-IVM.patch" Content-Disposition: attachment; filename="v30-0008-Add-aggregates-support-in-IVM.patch" Content-Transfer-Encoding: quoted-printable ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v31 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 320 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 04a5ee9e37..8f2bd5203e 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -50,6 +50,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -305,6 +306,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -409,6 +413,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -532,7 +579,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -936,10 +984,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1086,12 +1130,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1118,41 +1168,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index d9016ef487..fb5265e6c3 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1132,6 +1133,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 1061c37b2c..f2e8aa02a3 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -148,11 +148,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -267,6 +271,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -329,8 +334,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -510,8 +520,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1533,6 +1543,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1545,7 +1562,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2018,7 +2036,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2094,7 +2112,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2116,7 +2139,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2129,6 +2156,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2178,6 +2250,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 6741e721ae..dad09b9b0b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -56,6 +56,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/sequence.h" #include "commands/tablecmds.h" @@ -3789,6 +3790,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 3337b77ae6..c191f70a6f 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -510,6 +510,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index c4d01a441a..ffcab8cda2 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -361,6 +361,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 427b7325db..65aecc96a7 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1503,6 +1504,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1588,6 +1590,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2752,7 +2755,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3020,7 +3023,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3037,7 +3040,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3050,6 +3053,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3212,6 +3218,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6cc9a8d8bf..5d22dbcfcf 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 396ad1bb4c..6b47e66bfd 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b89baef95d..bfa48d659e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1089,6 +1089,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Fri__29_Mar_2024_23_47_00_+0900_KGpmmDOIs1266Ib1 Content-Type: text/x-diff; name="v31-0008-Add-aggregates-support-in-IVM.patch" Content-Disposition: attachment; filename="v31-0008-Add-aggregates-support-in-IVM.patch" Content-Transfer-Encoding: quoted-printable ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v32 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 2 + 10 files changed, 320 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index e9846c8d0f..299c5a133c 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -50,6 +50,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -305,6 +306,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -409,6 +413,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -532,7 +579,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -938,10 +986,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1088,12 +1132,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1120,41 +1170,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index d9016ef487..fb5265e6c3 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -41,6 +41,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1132,6 +1133,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 78a5dd1df9..0064e10966 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -148,11 +148,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -267,6 +271,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -329,8 +334,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -510,8 +520,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1535,6 +1545,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1547,7 +1564,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -2020,7 +2038,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2096,7 +2114,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2118,7 +2141,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2131,6 +2158,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2180,6 +2252,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index a28f405e27..12b5b5df64 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -56,6 +56,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/sequence.h" #include "commands/tablecmds.h" @@ -3797,6 +3798,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 3337b77ae6..c191f70a6f 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -510,6 +510,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_INT_FIELD(rellockmode); WRITE_UINT_FIELD(perminfoindex); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index c4d01a441a..ffcab8cda2 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -361,6 +361,7 @@ _readRangeTblEntry(void) READ_INT_FIELD(rellockmode); READ_UINT_FIELD(perminfoindex); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 427b7325db..65aecc96a7 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1503,6 +1504,7 @@ addRangeTableEntry(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1588,6 +1590,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->inh = inh; rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2752,7 +2755,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3020,7 +3023,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3037,7 +3040,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3050,6 +3053,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3212,6 +3218,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6cc9a8d8bf..5d22dbcfcf 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -614,7 +614,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 396ad1bb4c..6b47e66bfd 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index a690ebc6e5..2e96bce175 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1090,6 +1090,8 @@ typedef struct RangeTblEntry Index perminfoindex pg_node_attr(query_jumble_ignore); /* sampling info, or NULL */ struct TableSampleClause *tablesample; + /* incrementally maintainable materialized view? */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Sun__31_Mar_2024_22_59_31_+0900_msknEviJj08_wgqO Content-Type: text/x-diff; name="v32-0008-Add-aggregates-support-in-IVM.patch" Content-Disposition: attachment; filename="v32-0008-Add-aggregates-support-in-IVM.patch" Content-Transfer-Encoding: quoted-printable ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v29 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 1 + 8 files changed, 317 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 415f110516..076f35ee6b 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -53,6 +53,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "storage/smgr.h" #include "tcop/tcopprot.h" @@ -309,6 +310,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -413,6 +417,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -536,7 +583,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -940,10 +988,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1090,12 +1134,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1122,41 +1172,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index ab8b81b302..4811a1c8df 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -38,6 +38,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1104,6 +1105,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 39305f3c49..aa518f20ef 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -152,11 +152,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -271,6 +275,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -330,8 +335,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -511,8 +521,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1512,6 +1522,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1524,7 +1541,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -1997,7 +2015,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2073,7 +2091,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2095,7 +2118,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2108,6 +2135,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2157,6 +2229,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 47c900445c..adbd768e0d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/policy.h" #include "commands/sequence.h" @@ -3673,6 +3674,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..c257440414 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1502,6 +1503,7 @@ addRangeTableEntry(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1587,6 +1589,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2758,7 +2761,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3026,7 +3029,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3043,7 +3046,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3056,6 +3059,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3218,6 +3224,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index e36fc72e1e..f6dc7ba202 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -621,7 +621,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 09a64fa2e5..76a7873ebf 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index fef4c714b8..1a2b8fa09e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1073,6 +1073,7 @@ typedef struct RangeTblEntry int rellockmode; /* lock level that query requires on the rel */ struct TableSampleClause *tablesample; /* sampling info, or NULL */ Index perminfoindex; + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Mon__28_Aug_2023_11_52_52_+0900_hj6L5h176QaSGtg7 Content-Type: text/x-diff; name="v29-0008-Add-aggregates-support-in-IVM.patch" Content-Disposition: attachment; filename="v29-0008-Add-aggregates-support-in-IVM.patch" Content-Transfer-Encoding: quoted-printable ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v29 07/11] Add DISTINCT support for IVM @ 2023-05-31 10:08 Yugo Nagata <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Yugo Nagata @ 2023-05-31 10:08 UTC (permalink / raw) When IMMV is created with DISTINCT, multiplicity of tuples is counted and stored in "__ivm_count__" column, which is a hidden column of IMMV. The value in __ivm_count__ is updated when IMMV is maintained incrementally. A tuple in IMMV can be removed if and only if the count becomes zero. --- src/backend/commands/createas.c | 141 ++++++++++++++++++++------ src/backend/commands/indexcmds.c | 40 ++++++++ src/backend/commands/matview.c | 148 ++++++++++++++++++++++++++-- src/backend/commands/tablecmds.c | 9 ++ src/backend/parser/parse_relation.c | 18 +++- src/backend/rewrite/rewriteDefine.c | 3 +- src/include/commands/createas.h | 2 + src/include/nodes/parsenodes.h | 1 + 8 files changed, 317 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 415f110516..076f35ee6b 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -53,6 +53,7 @@ #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" +#include "parser/parse_func.h" #include "rewrite/rewriteHandler.h" #include "storage/smgr.h" #include "tcop/tcopprot.h" @@ -309,6 +310,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); + + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); } if (into->skipData) @@ -413,6 +417,49 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, return address; } +/* + * rewriteQueryForIMMV -- rewrite view definition query for IMMV + * + * count(*) is added for counting distinct tuples in views. + */ +Query * +rewriteQueryForIMMV(Query *query, List *colNames) +{ + Query *rewritten; + + Node *node; + ParseState *pstate = make_parsestate(NULL); + FuncCall *fn; + + rewritten = copyObject(query); + pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; + + /* + * Convert DISTINCT to GROUP BY and add count(*) for counting distinct + * tuples in views. + */ + if (rewritten->distinctClause) + { + TargetEntry *tle; + + rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); + + fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); + fn->agg_star = true; + + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(rewritten->targetList) + 1, + pstrdup("__ivm_count__"), + false); + rewritten->targetList = lappend(rewritten->targetList, tle); + rewritten->hasAggs = true; + } + + return rewritten; +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -536,7 +583,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ColumnDef *col; char *colname; - if (lc) + /* Don't override hidden columns added for IVM */ + if (lc && !isIvmName(NameStr(attribute->attname))) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); @@ -940,10 +988,6 @@ check_ivm_restriction_walker(Node *node, void *context) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); - if (qry->distinctClause) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -1090,12 +1134,18 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; - Bitmapset *key_attnos; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); + /* + * We consider null values not distinct to make sure that views with DISTINCT + * or GROUP BY don't contain multiple NULL rows when NULL is inserted to + * a base table concurrently. + */ + index->nulls_not_distinct = true; + index->unique = true; index->primary = false; index->isconstraint = false; @@ -1122,41 +1172,68 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->concurrent = false; index->if_not_exists = false; - /* create index on the base tables' primary key columns */ - key_attnos = get_primary_key_attnos_from_query(query, &constraintList); - if (key_attnos) + if (query->distinctClause) { + /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); - - if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) - { - IndexElem *iparam; - - iparam = makeNode(IndexElem); - iparam->name = pstrdup(NameStr(attr->attname)); - iparam->expr = NULL; - iparam->indexcolname = NULL; - iparam->collation = NIL; - iparam->opclass = NIL; - iparam->opclassopts = NIL; - iparam->ordering = SORTBY_DEFAULT; - iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; - index->indexParams = lappend(index->indexParams, iparam); - } + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); } } else { - /* create no index, just notice that an appropriate index is necessary for efficient IVM */ - ereport(NOTICE, - (errmsg("could not create an index on materialized view \"%s\" automatically", - RelationGetRelationName(matviewRel)), - errdetail("This target list does not have all the primary key columns. "), - errhint("Create an index on the materialized view for efficient incremental maintenance."))); - return; + Bitmapset *key_attnos; + + /* create index on the base tables' primary key columns */ + key_attnos = get_primary_key_attnos_from_query(query, &constraintList); + if (key_attnos) + { + foreach(lc, query->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); + + if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) + { + IndexElem *iparam; + + iparam = makeNode(IndexElem); + iparam->name = pstrdup(NameStr(attr->attname)); + iparam->expr = NULL; + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->opclassopts = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + } + } + } + else + { + /* create no index, just notice that an appropriate index is necessary for efficient IVM */ + ereport(NOTICE, + (errmsg("could not create an index on materialized view \"%s\" automatically", + RelationGetRelationName(matviewRel)), + errdetail("This target list does not have all the primary key columns, " + "or this view does not contain DISTINCT clause."), + errhint("Create an index on the materialized view for efficient incremental maintenance."))); + return; + } } /* If we have a compatible index, we don't need to create another. */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index ab8b81b302..4811a1c8df 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -38,6 +38,7 @@ #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/matview.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -1104,6 +1105,45 @@ DefineIndex(Oid tableId, safe_index = indexInfo->ii_Expressions == NIL && indexInfo->ii_Predicate == NIL; + /* + * We disallow unique indexes on IVM columns of IMMVs. + */ + if (RelationIsIVM(rel) && stmt->unique) + { + for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++) + { + AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i]; + if (attno > 0) + { + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + } + + if (indexInfo->ii_Expressions) + { + Bitmapset *indexattrs = NULL; + int varno = -1; + + pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs); + + while ((varno = bms_next_member(indexattrs, varno)) >= 0) + { + int attno = varno + FirstLowInvalidHeapAttributeNumber; + char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname); + if (name && isIvmName(name)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unique index creation on IVM columns is not supported"))); + } + + } + } + + /* * Report index creation if appropriate (delay this till after most of the * error checks) diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 39305f3c49..aa518f20ef 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -152,11 +152,15 @@ static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable * static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query); + Query *query, bool use_count, char *count_colname); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); +static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); +static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname); static char *get_matching_condition_string(List *keys); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); @@ -271,6 +275,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, Oid matviewOid; Relation matviewRel; Query *dataQuery; + Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; @@ -330,8 +335,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, "CONCURRENTLY", "WITH NO DATA"))); - dataQuery = get_matview_query(matviewRel); + viewQuery = get_matview_query(matviewRel); + /* For IMMV, we need to rewrite matview query */ + if (!stmt->skipData && RelationIsIVM(matviewRel)) + dataQuery = rewriteQueryForIMMV(viewQuery,NIL); + else + dataQuery = viewQuery; /* * Check that there is a unique index with no WHERE clause on one or more @@ -511,8 +521,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated) { - CreateIndexOnIMMV(dataQuery, matviewRel); - CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); + CreateIndexOnIMMV(viewQuery, matviewRel); + CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); } table_close(matviewRel, NoLock); @@ -1512,6 +1522,13 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) int rte_index = lfirst_int(lc2); TupleDesc tupdesc_old; TupleDesc tupdesc_new; + bool use_count = false; + char *count_colname = NULL; + + count_colname = pstrdup("__ivm_count__"); + + if (query->distinctClause) + use_count = true; /* calculate delta tables */ calc_delta(table, rte_index, rewritten, dest_old, dest_new, @@ -1524,7 +1541,8 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, - tupdesc_old, tupdesc_new, query); + tupdesc_old, tupdesc_new, query, use_count, + count_colname); } PG_CATCH(); { @@ -1997,7 +2015,7 @@ rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, - Query *query) + Query *query, bool use_count, char *count_colname) { StringInfoData querybuf; StringInfoData target_list_buf; @@ -2073,7 +2091,12 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); - apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); + if (use_count) + /* apply old delta and get rows to be recalculated */ + apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, + keys, count_colname); + else + apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); } /* For tuple insertion */ @@ -2095,7 +2118,11 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_register failed"); /* apply new delta */ - apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); + if (use_count) + apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, + keys, &target_list_buf, count_colname); + else + apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); } /* We're done maintaining the materialized view. */ @@ -2108,6 +2135,51 @@ apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *n elog(ERROR, "SPI_finish failed"); } +/* + * apply_old_delta_with_count + * + * Execute a query for applying a delta table given by deltname_old + * which contains tuples to be deleted from to a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. + */ +static void +apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, + List *keys, const char *count_colname) +{ + StringInfoData querybuf; + char *match_cond; + + /* build WHERE condition for searching tuples to be deleted */ + match_cond = get_matching_condition_string(keys); + + /* Search for matching tuples from the view and update or delete if found. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" /* collecting tid of target tuples in the view */ + "SELECT diff.%s, " /* count column */ + "(diff.%s OPERATOR(pg_catalog.=) mv.%s) AS for_dlt, " + "mv.ctid " + "FROM %s AS mv, %s AS diff " + "WHERE %s" /* tuple matching condition */ + "), updt AS (" /* update a tuple if this is not to be deleted */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " + "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " + ")" + /* delete a tuple if this is to be deleted */ + "DELETE FROM %s AS mv USING t " + "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt", + count_colname, + count_colname, count_colname, + matviewname, deltaname_old, + match_cond, + matviewname, count_colname, count_colname, count_colname, + matviewname); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_old_delta * @@ -2157,6 +2229,66 @@ apply_old_delta(const char *matviewname, const char *deltaname_old, elog(ERROR, "SPI_exec failed: %s", querybuf.data); } +/* + * apply_new_delta_with_count + * + * Execute a query for applying a delta table given by deltname_new + * which contains tuples to be inserted into a materialized view given by + * matviewname. This is used when counting is required, that is, the view + * has aggregate or distinct. Also, when a table in EXISTS sub queries + * is modified. + */ +static void +apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, + List *keys, StringInfo target_list, const char* count_colname) +{ + StringInfoData querybuf; + StringInfoData returning_keys; + ListCell *lc; + char *match_cond = ""; + + /* build WHERE condition for searching tuples to be updated */ + match_cond = get_matching_condition_string(keys); + + /* build string of keys list */ + initStringInfo(&returning_keys); + if (keys) + { + foreach (lc, keys) + { + Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); + char *resname = NameStr(attr->attname); + appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); + if (lnext(keys, lc)) + appendStringInfo(&returning_keys, ", "); + } + } + else + appendStringInfo(&returning_keys, "NULL"); + + /* Search for matching tuples from the view and update if found or insert if not. */ + initStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" /* update a tuple if this exists in the view */ + "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " + "FROM %s AS diff " + "WHERE %s " /* tuple matching condition */ + "RETURNING %s" /* returning keys of updated tuples */ + ") INSERT INTO %s (%s) " /* insert a new tuple if this doesn't exist */ + "SELECT %s FROM %s AS diff " + "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", + matviewname, count_colname, count_colname, count_colname, + deltaname_new, + match_cond, + returning_keys.data, + matviewname, target_list->data, + target_list->data, deltaname_new, + match_cond); + + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); +} + /* * apply_new_delta * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 47c900445c..adbd768e0d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/matview.h" #include "commands/event_trigger.h" #include "commands/policy.h" #include "commands/sequence.h" @@ -3673,6 +3674,14 @@ renameatt_internal(Oid myrelid, targetrelation = relation_open(myrelid, AccessExclusiveLock); renameatt_check(myrelid, RelationGetForm(targetrelation), recursing); + /* + * Don't rename IVM columns. + */ + if (RelationIsIVM(targetrelation) && isIvmName(oldattname)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("IVM column can not be renamed"))); + /* * if the 'recurse' flag is set then we are supposed to rename this * attribute in all classes that inherit from 'relname' (as well as in diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 864ea9b0d5..c257440414 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -36,6 +36,7 @@ #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" /* @@ -97,7 +98,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars); + List **colnames, List **colvars, bool is_ivm); static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); @@ -1502,6 +1503,7 @@ addRangeTableEntry(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1587,6 +1589,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2758,7 +2761,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, rtindex, sublevels_up, location, - include_dropped, colnames, colvars); + include_dropped, colnames, colvars, false); } else if (functypclass == TYPEFUNC_SCALAR) { @@ -3026,7 +3029,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, rtindex, sublevels_up, location, include_dropped, - colnames, colvars); + colnames, colvars, RelationIsIVM(rel)); relation_close(rel, AccessShareLock); } @@ -3043,7 +3046,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, int location, bool include_dropped, - List **colnames, List **colvars) + List **colnames, List **colvars, bool is_ivm) { ListCell *aliascell; int varattno; @@ -3056,6 +3059,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) @@ -3218,6 +3224,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, Var *varnode = (Var *) lfirst(var); TargetEntry *te; + /* if transform * into columnlist with IMMV, remove IVM columns */ + if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + te = makeTargetEntry((Expr *) varnode, (AttrNumber) pstate->p_next_resno++, label, diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index e36fc72e1e..f6dc7ba202 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -621,7 +621,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 09a64fa2e5..76a7873ebf 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -29,6 +29,8 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); +extern Query *rewriteQueryForIMMV(Query *query, List *colNames); + extern int GetIntoRelEFlags(IntoClause *intoClause); extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index fef4c714b8..1a2b8fa09e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1073,6 +1073,7 @@ typedef struct RangeTblEntry int rellockmode; /* lock level that query requires on the rel */ struct TableSampleClause *tablesample; /* sampling info, or NULL */ Index perminfoindex; + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): -- 2.25.1 --Multipart=_Mon__28_Aug_2023_16_05_30_+0900_b1OvQD_3A3ZMTGvj Content-Type: text/x-diff; name="v29-0008-Add-aggregates-support-in-IVM.patch" Content-Disposition: attachment; filename="v29-0008-Add-aggregates-support-in-IVM.patch" Content-Transfer-Encoding: quoted-printable ^ permalink raw reply [nested|flat] 383+ messages in thread
* [PATCH v11] Avoid orphaned objects dependencies @ 2024-03-29 15:43 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 383+ messages in thread From: Bertrand Drouvot @ 2024-03-29 15:43 UTC (permalink / raw) It's currently possible to create orphaned objects dependencies, for example: Scenario 1: session 1: begin; drop schema schem; session 2: create a function in the schema schem session 1: commit; With the above, the function created in session 2 would be linked to a non existing schema. Scenario 2: session 1: begin; create a function in the schema schem session 2: drop schema schem; session 1: commit; With the above, the function created in session 1 would be linked to a non existing schema. To avoid those scenarios, a new lock (that conflicts with a lock taken by DROP) has been put in place before the dependencies are being recorded. With this in place, the drop schema in scenario 2 would be locked. Also, after the new lock attempt, the patch checks that the object still exists: with this in place session 2 in scenario 1 would be locked and would report an error once session 1 committs (that would not be the case should session 1 abort the transaction). If the object is dropped before the new lock attempt is triggered then the patch would also report an error (but with less details). The patch takes into account any type of objects except: - the ones that are pinned - the ones that belongs to the RelationRelationId class For the former, that's due to the fact that pinned objects are not droppable because the system requires it. For the later, that because we must be in one of the two following cases that would already prevent the relation to be dropped: 1. The relation is already locked (could be an existing relation or a relation that we are creating). 2. The relation is protected indirectly (i.e an index protected by a lock on its table, a table protected by a lock on a function that depends the table...) The patch adds a few tests for some dependency cases (that would currently produce orphaned objects): - schema and function (as the above scenarios) - alter a dependency (function and schema) - function and arg type - function and return type - function and function - domain and domain - table and type - server and foreign data wrapper --- src/backend/catalog/aclchk.c | 1 + src/backend/catalog/dependency.c | 115 +++++++++++++++- src/backend/catalog/heap.c | 7 + src/backend/catalog/index.c | 40 ++++++ src/backend/catalog/objectaddress.c | 57 ++++++++ src/backend/catalog/pg_aggregate.c | 9 ++ src/backend/catalog/pg_attrdef.c | 4 + src/backend/catalog/pg_cast.c | 5 + src/backend/catalog/pg_collation.c | 1 + src/backend/catalog/pg_constraint.c | 37 +++++ src/backend/catalog/pg_conversion.c | 2 + src/backend/catalog/pg_depend.c | 26 +++- src/backend/catalog/pg_operator.c | 19 +++ src/backend/catalog/pg_proc.c | 17 ++- src/backend/catalog/pg_publication.c | 13 ++ src/backend/catalog/pg_range.c | 6 + src/backend/catalog/pg_type.c | 38 ++++++ src/backend/catalog/toasting.c | 4 + src/backend/commands/alter.c | 4 + src/backend/commands/amcmds.c | 1 + src/backend/commands/cluster.c | 13 ++ src/backend/commands/event_trigger.c | 1 + src/backend/commands/extension.c | 5 + src/backend/commands/foreigncmds.c | 7 + src/backend/commands/functioncmds.c | 6 + src/backend/commands/indexcmds.c | 2 + src/backend/commands/opclasscmds.c | 18 +++ src/backend/commands/operatorcmds.c | 30 ++++ src/backend/commands/policy.c | 8 ++ src/backend/commands/proclang.c | 3 + src/backend/commands/sequence.c | 5 + src/backend/commands/statscmds.c | 11 ++ src/backend/commands/tablecmds.c | 43 ++++-- src/backend/commands/trigger.c | 39 +++++- src/backend/commands/tsearchcmds.c | 81 +++++++---- src/backend/commands/typecmds.c | 84 ++++++++++++ src/backend/rewrite/rewriteDefine.c | 4 + src/backend/utils/errcodes.txt | 1 + src/include/catalog/dependency.h | 7 + src/include/catalog/objectaddress.h | 1 + .../expected/test_dependencies_locks.out | 129 ++++++++++++++++++ src/test/isolation/isolation_schedule | 1 + .../specs/test_dependencies_locks.spec | 89 ++++++++++++ 43 files changed, 947 insertions(+), 47 deletions(-) 33.4% src/backend/catalog/ 36.6% src/backend/commands/ 17.1% src/test/isolation/expected/ 10.6% src/test/isolation/specs/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 143876b77f..5a614f25ff 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -1413,6 +1413,7 @@ SetDefaultACL(InternalDefaultACL *iacls) referenced.objectId = iacls->nspid; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, iacls->nspid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); } } diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 0489cbabcb..b779872c3e 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1519,6 +1519,90 @@ AcquireDeletionLock(const ObjectAddress *object, int flags) } } +/* + * LockNotPinnedObjectById + * + * Lock the object that we are about to record a dependency on. + * After it's locked, verify that it hasn't been dropped while we + * weren't looking. If the object has been dropped, this function + * does not return! + */ +void +LockNotPinnedObjectById(const ObjectAddress *object) +{ + char *object_description; + + if (isObjectPinned(object)) + return; + + /* + * We must be in one of the two following cases that would already prevent + * the relation to be dropped: 1. The relation is already locked (could be + * an existing relation or a relation that we are creating). 2. The + * relation is protected indirectly (i.e an index protected by a lock on + * its table, a table protected by a lock on a function that depends of + * the table...). So don't add a lock if this is a RelationRelationId + * object class (would need to make use of LockRelationOid() though). Note + * that, due to 2., adding an assertion that we already hold a lock on the + * relation is not doable. + */ + if (object->classId == RelationRelationId) + return; + + object_description = getObjectDescription(object, true); + + /* assume we should lock the whole object not a sub-object */ + LockDatabaseObject(object->classId, object->objectId, 0, AccessShareLock); + + /* check if object still exists */ + if (!ObjectByIdExist(object)) + { + if (object_description) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_DOES_NOT_EXIST), + errmsg("%s does not exist", object_description))); + else + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_DOES_NOT_EXIST), + errmsg("a dependent object does not exist"))); + } + + if (object_description) + pfree(object_description); + + return; +} + +void +LockNotPinnedObjectsById(const ObjectAddress *object, int nobject) +{ + int i; + + if (nobject < 0) + return; + + for (i = 0; i < nobject; i++, object++) + LockNotPinnedObjectById(object); + + return; +} + + +/* + * LockNotPinnedObject + * + * Lock the object that we are about to record a dependency on. + */ +void +LockNotPinnedObject(Oid classid, Oid objid) +{ + ObjectAddress object; + + ObjectAddressSet(object, classid, objid); + + LockNotPinnedObjectById(&object); +} + /* * ReleaseDeletionLock - release an object deletion lock * @@ -1564,13 +1648,8 @@ recordDependencyOnExpr(const ObjectAddress *depender, /* Scan the expression tree for referenceable objects */ find_expr_references_walker(expr, &context); - /* Remove any duplicates */ - eliminate_duplicate_dependencies(context.addrs); - - /* And record 'em */ - recordMultipleDependencies(depender, - context.addrs->refs, context.addrs->numrefs, - behavior); + /* Record all of them (this includes duplicate elimination) */ + lock_record_object_address_dependencies(depender, context.addrs, behavior); free_object_addresses(context.addrs); } @@ -1654,14 +1733,19 @@ recordDependencyOnSingleRelExpr(const ObjectAddress *depender, /* Record the self-dependencies with the appropriate direction */ if (!reverse_self) + { + LockNotPinnedObjectsById(self_addrs->refs, self_addrs->numrefs); recordMultipleDependencies(depender, self_addrs->refs, self_addrs->numrefs, self_behavior); + } else { /* Can't use recordMultipleDependencies, so do it the hard way */ int selfref; + LockNotPinnedObjectById(depender); + for (selfref = 0; selfref < self_addrs->numrefs; selfref++) { ObjectAddress *thisobj = self_addrs->refs + selfref; @@ -1674,6 +1758,7 @@ recordDependencyOnSingleRelExpr(const ObjectAddress *depender, } /* Record the external dependencies */ + LockNotPinnedObjectsById(context.addrs->refs, context.addrs->numrefs); recordMultipleDependencies(depender, context.addrs->refs, context.addrs->numrefs, behavior); @@ -2734,6 +2819,22 @@ stack_address_present_add_flags(const ObjectAddress *object, return result; } +/* + * Record multiple dependencies from an ObjectAddresses array and lock the + * referenced objects, after first removing any duplicates. + */ +void +lock_record_object_address_dependencies(const ObjectAddress *depender, + ObjectAddresses *referenced, + DependencyType behavior) +{ + eliminate_duplicate_dependencies(referenced); + LockNotPinnedObjectsById(referenced->refs, referenced->numrefs); + recordMultipleDependencies(depender, + referenced->refs, referenced->numrefs, + behavior); +} + /* * Record multiple dependencies from an ObjectAddresses array, after first * removing any duplicates. diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index a122bbffce..00977e56c4 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -843,6 +843,7 @@ AddNewAttributeTuples(Oid new_rel_oid, ObjectAddressSubSet(myself, RelationRelationId, new_rel_oid, i + 1); ObjectAddressSet(referenced, TypeRelationId, tupdesc->attrs[i].atttypid); + LockNotPinnedObject(TypeRelationId, tupdesc->attrs[i].atttypid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* The default collation is pinned, so don't bother recording it */ @@ -851,6 +852,7 @@ AddNewAttributeTuples(Oid new_rel_oid, { ObjectAddressSet(referenced, CollationRelationId, tupdesc->attrs[i].attcollation); + LockNotPinnedObject(CollationRelationId, tupdesc->attrs[i].attcollation); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } } @@ -1451,11 +1453,13 @@ heap_create_with_catalog(const char *relname, ObjectAddressSet(referenced, NamespaceRelationId, relnamespace); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(NamespaceRelationId, relnamespace); if (reloftypeid) { ObjectAddressSet(referenced, TypeRelationId, reloftypeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, reloftypeid); } /* @@ -1469,6 +1473,7 @@ heap_create_with_catalog(const char *relname, { ObjectAddressSet(referenced, AccessMethodRelationId, accessmtd); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(AccessMethodRelationId, accessmtd); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -3383,6 +3388,7 @@ StorePartitionKey(Relation rel, { ObjectAddressSet(referenced, OperatorClassRelationId, partopclass[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorClassRelationId, partopclass[i]); /* The default collation is pinned, so don't bother recording it */ if (OidIsValid(partcollation[i]) && @@ -3390,6 +3396,7 @@ StorePartitionKey(Relation rel, { ObjectAddressSet(referenced, CollationRelationId, partcollation[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CollationRelationId, partcollation[i]); } } diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 55fdde4b24..445ea5a846 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1127,6 +1127,12 @@ index_create(Relation heapRelation, heapRelationId, indexInfo->ii_IndexAttrNumbers[i]); add_exact_object_address(&referenced, addrs); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on heapRelationId as it is + * already locked. + */ have_simple_col = true; } } @@ -1142,6 +1148,12 @@ index_create(Relation heapRelation, ObjectAddressSet(referenced, RelationRelationId, heapRelationId); add_exact_object_address(&referenced, addrs); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on heapRelationId as it is already + * locked. + */ } record_object_address_dependencies(&myself, addrs, DEPENDENCY_AUTO); @@ -1157,9 +1169,21 @@ index_create(Relation heapRelation, if (OidIsValid(parentIndexRelid)) { ObjectAddressSet(referenced, RelationRelationId, parentIndexRelid); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on parentIndexRelid as it is already + * locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, heapRelationId); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on heapRelationId as it is already + * locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC); } @@ -1175,6 +1199,7 @@ index_create(Relation heapRelation, { ObjectAddressSet(referenced, CollationRelationId, collationIds[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CollationRelationId, collationIds[i]); } } @@ -1183,6 +1208,7 @@ index_create(Relation heapRelation, { ObjectAddressSet(referenced, OperatorClassRelationId, opclassIds[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorClassRelationId, opclassIds[i]); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -1987,6 +2013,14 @@ index_constraint_create(Relation heapRelation, */ ObjectAddressSet(myself, ConstraintRelationId, conOid); ObjectAddressSet(idxaddr, RelationRelationId, indexRelationId); + + /* + * CommandCounterIncrement() here to ensure the new constraint entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + LockNotPinnedObject(ConstraintRelationId, conOid); recordDependencyOn(&idxaddr, &myself, DEPENDENCY_INTERNAL); /* @@ -1998,9 +2032,15 @@ index_constraint_create(Relation heapRelation, ObjectAddress referenced; ObjectAddressSet(referenced, ConstraintRelationId, parentConstraintId); + LockNotPinnedObject(ConstraintRelationId, parentConstraintId); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(heapRelation)); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on heapRelation as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC); } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 7b536ac6fd..6d7abd3738 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2590,6 +2590,63 @@ get_object_namespace(const ObjectAddress *address) return oid; } +/* + * ObjectByIdExist + * + * Return whether the given object exists. + * + * Works for most catalogs, if no special processing is needed. + */ +bool +ObjectByIdExist(const ObjectAddress *address) +{ + HeapTuple tuple; + int cache; + const ObjectPropertyType *property; + + property = get_object_property_data(address->classId); + + cache = property->oid_catcache_id; + + if (cache >= 0) + { + /* Fetch tuple from syscache. */ + tuple = SearchSysCache1(cache, ObjectIdGetDatum(address->objectId)); + + if (!HeapTupleIsValid(tuple)) + { + return false; + } + + ReleaseSysCache(tuple); + + return true; + } + else + { + Relation rel; + ScanKeyData skey[1]; + SysScanDesc scan; + + rel = table_open(address->classId, AccessShareLock); + + ScanKeyInit(&skey[0], + get_object_attnum_oid(address->classId), + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(address->objectId)); + + scan = systable_beginscan(rel, get_object_oid_index(address->classId), true, + NULL, 1, skey); + + /* we expect exactly one match */ + tuple = systable_getnext(scan); + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return (HeapTupleIsValid(tuple)); + } +} + /* * Return ObjectType for the given object type as given by * getObjectTypeDescription; if no valid ObjectType code exists, but it's a diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c index 90fc7db949..a47e3c5507 100644 --- a/src/backend/catalog/pg_aggregate.c +++ b/src/backend/catalog/pg_aggregate.c @@ -748,12 +748,14 @@ AggregateCreate(const char *aggName, /* Depends on transition function */ ObjectAddressSet(referenced, ProcedureRelationId, transfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, transfn); /* Depends on final function, if any */ if (OidIsValid(finalfn)) { ObjectAddressSet(referenced, ProcedureRelationId, finalfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, finalfn); } /* Depends on combine function, if any */ @@ -761,6 +763,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, combinefn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, combinefn); } /* Depends on serialization function, if any */ @@ -768,6 +771,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, serialfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, serialfn); } /* Depends on deserialization function, if any */ @@ -775,6 +779,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, deserialfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, deserialfn); } /* Depends on forward transition function, if any */ @@ -782,6 +787,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, mtransfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, mtransfn); } /* Depends on inverse transition function, if any */ @@ -789,6 +795,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, minvtransfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, minvtransfn); } /* Depends on final function, if any */ @@ -796,6 +803,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, mfinalfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, mfinalfn); } /* Depends on sort operator, if any */ @@ -803,6 +811,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, OperatorRelationId, sortop); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorRelationId, sortop); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); diff --git a/src/backend/catalog/pg_attrdef.c b/src/backend/catalog/pg_attrdef.c index 003ae70b4d..21491613e3 100644 --- a/src/backend/catalog/pg_attrdef.c +++ b/src/backend/catalog/pg_attrdef.c @@ -178,6 +178,10 @@ StoreAttrDefault(Relation rel, AttrNumber attnum, colobject.objectId = RelationGetRelid(rel); colobject.objectSubId = attnum; + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) on + * rel as it is already locked. + */ recordDependencyOn(&defobject, &colobject, attgenerated ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); diff --git a/src/backend/catalog/pg_cast.c b/src/backend/catalog/pg_cast.c index 5a5b855d51..d3707e424c 100644 --- a/src/backend/catalog/pg_cast.c +++ b/src/backend/catalog/pg_cast.c @@ -97,16 +97,19 @@ CastCreate(Oid sourcetypeid, Oid targettypeid, /* dependency on source type */ ObjectAddressSet(referenced, TypeRelationId, sourcetypeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, sourcetypeid); /* dependency on target type */ ObjectAddressSet(referenced, TypeRelationId, targettypeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, targettypeid); /* dependency on function */ if (OidIsValid(funcid)) { ObjectAddressSet(referenced, ProcedureRelationId, funcid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, funcid); } /* dependencies on casts required for function */ @@ -114,11 +117,13 @@ CastCreate(Oid sourcetypeid, Oid targettypeid, { ObjectAddressSet(referenced, CastRelationId, incastid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CastRelationId, incastid); } if (OidIsValid(outcastid)) { ObjectAddressSet(referenced, CastRelationId, outcastid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CastRelationId, outcastid); } record_object_address_dependencies(&myself, addrs, behavior); diff --git a/src/backend/catalog/pg_collation.c b/src/backend/catalog/pg_collation.c index 7f2f701229..78498b8c20 100644 --- a/src/backend/catalog/pg_collation.c +++ b/src/backend/catalog/pg_collation.c @@ -218,6 +218,7 @@ CollationCreate(const char *collname, Oid collnamespace, referenced.classId = NamespaceRelationId; referenced.objectId = collnamespace; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, collnamespace); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* create dependency on owner */ diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 3baf9231ed..c7c1115bec 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -257,12 +257,22 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSubSet(relobject, RelationRelationId, relId, constraintKey[i]); add_exact_object_address(&relobject, addrs_auto); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on relId as it is already locked. + */ } } else { ObjectAddressSet(relobject, RelationRelationId, relId); add_exact_object_address(&relobject, addrs_auto); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on relId as it is already locked. + */ } } @@ -275,6 +285,7 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(domobject, TypeRelationId, domainId); add_exact_object_address(&domobject, addrs_auto); + LockNotPinnedObject(TypeRelationId, domainId); } record_object_address_dependencies(&conobject, addrs_auto, @@ -299,12 +310,23 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSubSet(relobject, RelationRelationId, foreignRelId, foreignKey[i]); add_exact_object_address(&relobject, addrs_normal); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on foreignRelId as it is already + * locked. + */ } } else { ObjectAddressSet(relobject, RelationRelationId, foreignRelId); add_exact_object_address(&relobject, addrs_normal); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on foreignRelId as it is already locked. + */ } } @@ -320,6 +342,12 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(relobject, RelationRelationId, indexRelId); add_exact_object_address(&relobject, addrs_normal); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on indexRelId as it can't be dropped as its table is already + * locked. + */ } if (foreignNKeys > 0) @@ -339,15 +367,18 @@ CreateConstraintEntry(const char *constraintName, { oprobject.objectId = pfEqOp[i]; add_exact_object_address(&oprobject, addrs_normal); + LockNotPinnedObject(OperatorRelationId, pfEqOp[i]); if (ppEqOp[i] != pfEqOp[i]) { oprobject.objectId = ppEqOp[i]; add_exact_object_address(&oprobject, addrs_normal); + LockNotPinnedObject(OperatorRelationId, ppEqOp[i]); } if (ffEqOp[i] != pfEqOp[i]) { oprobject.objectId = ffEqOp[i]; add_exact_object_address(&oprobject, addrs_normal); + LockNotPinnedObject(OperatorRelationId, ffEqOp[i]); } } } @@ -858,9 +889,15 @@ ConstraintSetParentConstraint(Oid childConstrId, ObjectAddressSet(depender, ConstraintRelationId, childConstrId); ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId); + LockNotPinnedObject(ConstraintRelationId, parentConstrId); recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, childTableId); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on childTableId as it is already locked. + */ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC); } else diff --git a/src/backend/catalog/pg_conversion.c b/src/backend/catalog/pg_conversion.c index 0770878eac..25881654d6 100644 --- a/src/backend/catalog/pg_conversion.c +++ b/src/backend/catalog/pg_conversion.c @@ -116,12 +116,14 @@ ConversionCreate(const char *conname, Oid connamespace, referenced.classId = ProcedureRelationId; referenced.objectId = conproc; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, conproc); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* create dependency on namespace */ referenced.classId = NamespaceRelationId; referenced.objectId = connamespace; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, connamespace); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* create dependency on owner */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index cfd7ef51df..f6f9764ec9 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -20,21 +20,20 @@ #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" +#include "catalog/pg_auth_members.h" #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" #include "catalog/pg_extension.h" #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/syscache.h" #include "utils/rel.h" -static bool isObjectPinned(const ObjectAddress *object); - - /* * Record a dependency between 2 objects via their respective objectAddress. * The first argument is the dependent object, the second the one it @@ -100,6 +99,24 @@ recordMultipleDependencies(const ObjectAddress *depender, slot_init_count = 0; for (i = 0; i < nreferenced; i++, referenced++) { +#ifdef USE_ASSERT_CHECKING + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, + MyDatabaseId, + referenced->classId, + referenced->objectId, + 0); + + /* + * Assert the referenced object is locked (see + * LockNotPinnedObjectById()). + */ + Assert(isObjectPinned(referenced) || + referenced->classId == RelationRelationId || + LockHeldByMe(&tag, AccessShareLock)); +#endif + /* * If the referenced object is pinned by the system, there's no real * need to record dependencies on it. This saves lots of space in @@ -239,6 +256,7 @@ recordDependencyOnCurrentExtension(const ObjectAddress *object, extension.objectId = CurrentExtensionObject; extension.objectSubId = 0; + LockNotPinnedObject(ExtensionRelationId, CurrentExtensionObject); recordDependencyOn(object, &extension, DEPENDENCY_EXTENSION); } } @@ -706,7 +724,7 @@ changeDependenciesOn(Oid refClassId, Oid oldRefObjectId, * The passed subId, if any, is ignored; we assume that only whole objects * are pinned (and that this implies pinning their components). */ -static bool +bool isObjectPinned(const ObjectAddress *object) { return IsPinnedObject(object->classId, object->objectId); diff --git a/src/backend/catalog/pg_operator.c b/src/backend/catalog/pg_operator.c index 65b45a424a..e8374eec88 100644 --- a/src/backend/catalog/pg_operator.c +++ b/src/backend/catalog/pg_operator.c @@ -251,6 +251,16 @@ OperatorShellMake(const char *operatorName, values[Anum_pg_operator_oprrest - 1] = ObjectIdGetDatum(InvalidOid); values[Anum_pg_operator_oprjoin - 1] = ObjectIdGetDatum(InvalidOid); + /* Lock dependent objects */ + if (OidIsValid(operatorNamespace)) + LockNotPinnedObject(NamespaceRelationId, operatorNamespace); + + if (OidIsValid(leftTypeId)) + LockNotPinnedObject(TypeRelationId, leftTypeId); + + if (OidIsValid(rightTypeId)) + LockNotPinnedObject(TypeRelationId, rightTypeId); + /* * create a new operator tuple */ @@ -513,6 +523,15 @@ OperatorCreate(const char *operatorName, CatalogTupleInsert(pg_operator_desc, tup); } + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, operatorNamespace); + LockNotPinnedObject(TypeRelationId, leftTypeId); + LockNotPinnedObject(TypeRelationId, rightTypeId); + LockNotPinnedObject(TypeRelationId, operResultType); + LockNotPinnedObject(ProcedureRelationId, procedureId); + LockNotPinnedObject(ProcedureRelationId, restrictionId); + LockNotPinnedObject(ProcedureRelationId, joinId); + /* Add dependencies for the entry */ address = makeOperatorDependencies(tup, true, isUpdate); diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index 528c17cd7f..116e524390 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -593,6 +593,13 @@ ProcedureCreate(const char *procedureName, if (is_update) deleteDependencyRecordsFor(ProcedureRelationId, retval, true); + /* + * CommandCounterIncrement() here to ensure the new function entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + addrs = new_object_addresses(); ObjectAddressSet(myself, ProcedureRelationId, retval); @@ -600,20 +607,24 @@ ProcedureCreate(const char *procedureName, /* dependency on namespace */ ObjectAddressSet(referenced, NamespaceRelationId, procNamespace); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(NamespaceRelationId, procNamespace); /* dependency on implementation language */ ObjectAddressSet(referenced, LanguageRelationId, languageObjectId); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(LanguageRelationId, languageObjectId); /* dependency on return type */ ObjectAddressSet(referenced, TypeRelationId, returnType); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, returnType); /* dependency on transform used by return type, if any */ if ((trfid = get_transform_oid(returnType, languageObjectId, true))) { ObjectAddressSet(referenced, TransformRelationId, trfid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TransformRelationId, trfid); } /* dependency on parameter types */ @@ -621,12 +632,14 @@ ProcedureCreate(const char *procedureName, { ObjectAddressSet(referenced, TypeRelationId, allParams[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, allParams[i]); /* dependency on transform used by parameter type, if any */ if ((trfid = get_transform_oid(allParams[i], languageObjectId, true))) { ObjectAddressSet(referenced, TransformRelationId, trfid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TransformRelationId, trfid); } } @@ -635,6 +648,7 @@ ProcedureCreate(const char *procedureName, { ObjectAddressSet(referenced, ProcedureRelationId, prosupport); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, prosupport); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -674,9 +688,6 @@ ProcedureCreate(const char *procedureName, ArrayType *set_items = NULL; int save_nestlevel = 0; - /* Advance command counter so new tuple can be seen by validator */ - CommandCounterIncrement(); - /* * Set per-function configuration parameters so that the validation is * done with the environment the function expects. However, if diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 0602398a54..e546c0a6fa 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -438,10 +438,16 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, /* Add dependency on the publication */ ObjectAddressSet(referenced, PublicationRelationId, pubid); + LockNotPinnedObject(PublicationRelationId, pubid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Add dependency on the relation */ ObjectAddressSet(referenced, RelationRelationId, relid); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) on + * relid as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Add dependency on the objects mentioned in the qualifications */ @@ -454,6 +460,11 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, for (int i = 0; i < natts; i++) { ObjectAddressSubSet(referenced, RelationRelationId, relid, attarray[i]); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on relid as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -661,10 +672,12 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists) /* Add dependency on the publication */ ObjectAddressSet(referenced, PublicationRelationId, pubid); + LockNotPinnedObject(PublicationRelationId, pubid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Add dependency on the schema */ ObjectAddressSet(referenced, NamespaceRelationId, schemaid); + LockNotPinnedObject(NamespaceRelationId, schemaid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Close the table */ diff --git a/src/backend/catalog/pg_range.c b/src/backend/catalog/pg_range.c index 501a6ba410..e5b5a0b6f8 100644 --- a/src/backend/catalog/pg_range.c +++ b/src/backend/catalog/pg_range.c @@ -70,26 +70,31 @@ RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, ObjectAddressSet(referenced, TypeRelationId, rangeSubType); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, rangeSubType); ObjectAddressSet(referenced, OperatorClassRelationId, rangeSubOpclass); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorClassRelationId, rangeSubOpclass); if (OidIsValid(rangeCollation)) { ObjectAddressSet(referenced, CollationRelationId, rangeCollation); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CollationRelationId, rangeCollation); } if (OidIsValid(rangeCanonical)) { ObjectAddressSet(referenced, ProcedureRelationId, rangeCanonical); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, rangeCanonical); } if (OidIsValid(rangeSubDiff)) { ObjectAddressSet(referenced, ProcedureRelationId, rangeSubDiff); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, rangeSubDiff); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -99,6 +104,7 @@ RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, referencing.classId = TypeRelationId; referencing.objectId = multirangeTypeOid; referencing.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, rangeTypeOid); recordDependencyOn(&referencing, &myself, DEPENDENCY_INTERNAL); table_close(pg_range, RowExclusiveLock); diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c index 395dec8ed8..5b36ff383a 100644 --- a/src/backend/catalog/pg_type.c +++ b/src/backend/catalog/pg_type.c @@ -157,6 +157,12 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId) * Create dependencies. We can/must skip this in bootstrap mode. */ if (!IsBootstrapProcessingMode()) + { + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, typeNamespace); + LockNotPinnedObject(ProcedureRelationId, F_SHELL_IN); + LockNotPinnedObject(ProcedureRelationId, F_SHELL_OUT); + GenerateTypeDependencies(tup, pg_type_desc, NULL, @@ -166,6 +172,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId) false, true, /* make extension dependency */ false); + } /* Post creation hook for new shell type */ InvokeObjectPostCreateHook(TypeRelationId, typoid, 0); @@ -494,6 +501,36 @@ TypeCreate(Oid newTypeOid, * Create dependencies. We can/must skip this in bootstrap mode. */ if (!IsBootstrapProcessingMode()) + { + /* + * CommandCounterIncrement() here to ensure the new type entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, typeNamespace); + LockNotPinnedObject(ProcedureRelationId, inputProcedure); + LockNotPinnedObject(ProcedureRelationId, outputProcedure); + LockNotPinnedObject(ProcedureRelationId, receiveProcedure); + LockNotPinnedObject(ProcedureRelationId, sendProcedure); + LockNotPinnedObject(ProcedureRelationId, typmodinProcedure); + LockNotPinnedObject(ProcedureRelationId, typmodoutProcedure); + LockNotPinnedObject(ProcedureRelationId, analyzeProcedure); + LockNotPinnedObject(ProcedureRelationId, subscriptProcedure); + LockNotPinnedObject(TypeRelationId, baseType); + LockNotPinnedObject(CollationRelationId, typeCollation); + LockNotPinnedObject(TypeRelationId, elementType); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on relationOid as relationOid is set to an InvalidOid or to a new + * Oid. + */ + if (relationKind == RELKIND_COMPOSITE_TYPE) + LockNotPinnedObject(TypeRelationId, typeObjectId); + GenerateTypeDependencies(tup, pg_type_desc, (defaultTypeBin ? @@ -505,6 +542,7 @@ TypeCreate(Oid newTypeOid, isDependentType, true, /* make extension dependency */ rebuildDeps); + } /* Post creation hook for new type */ InvokeObjectPostCreateHook(TypeRelationId, typeObjectId, 0); diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 738bc46ae8..6705bc24b8 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -367,6 +367,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, toastobject.objectId = toast_relid; toastobject.objectSubId = 0; + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on relOid as it is already locked. + */ recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL); } diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index 4f99ebb447..57e86f576a 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -501,7 +501,10 @@ ExecAlterObjectDependsStmt(AlterObjectDependsStmt *stmt, ObjectAddress *refAddre currexts = getAutoExtensionsOfObject(address.classId, address.objectId); if (!list_member_oid(currexts, refAddr.objectId)) + { + LockNotPinnedObject(refAddr.classId, refAddr.objectId); recordDependencyOn(&address, &refAddr, DEPENDENCY_AUTO_EXTENSION); + } } return address; @@ -807,6 +810,7 @@ AlterObjectNamespace_internal(Relation rel, Oid objid, Oid nspOid) pfree(replaces); /* update dependency to point to the new schema */ + LockNotPinnedObject(NamespaceRelationId, nspOid); if (changeDependencyFor(classId, objid, NamespaceRelationId, oldNspOid, nspOid) != 1) elog(ERROR, "could not change schema dependency for object %u", diff --git a/src/backend/commands/amcmds.c b/src/backend/commands/amcmds.c index aaa0f9a1dc..8616a7c9fa 100644 --- a/src/backend/commands/amcmds.c +++ b/src/backend/commands/amcmds.c @@ -104,6 +104,7 @@ CreateAccessMethod(CreateAmStmt *stmt) referenced.objectId = amhandler; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, amhandler); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 78f96789b0..5d8b2948d8 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -1272,6 +1272,7 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, */ if (relam1 != relam2) { + LockNotPinnedObject(AccessMethodRelationId, relam2); if (changeDependencyFor(RelationRelationId, r1, AccessMethodRelationId, @@ -1280,6 +1281,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, elog(ERROR, "could not change access method dependency for relation \"%s.%s\"", get_namespace_name(get_rel_namespace(r1)), get_rel_name(r1)); + + LockNotPinnedObject(AccessMethodRelationId, relam1); if (changeDependencyFor(RelationRelationId, r2, AccessMethodRelationId, @@ -1381,6 +1384,11 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, { baseobject.objectId = r1; toastobject.objectId = relform1->reltoastrelid; + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on r1 as it is already locked. + */ recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL); } @@ -1389,6 +1397,11 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, { baseobject.objectId = r2; toastobject.objectId = relform2->reltoastrelid; + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on r2 as it is already locked. + */ recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL); } diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 7a5ed6b985..8d0cdec59e 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -327,6 +327,7 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO referenced.classId = ProcedureRelationId; referenced.objectId = funcoid; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, funcoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* Depend on extension, if any. */ diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 1643c8c69a..669a5d6dd8 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -1924,6 +1924,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner, ObjectAddressSet(nsp, NamespaceRelationId, schemaOid); add_exact_object_address(&nsp, refobjs); + LockNotPinnedObject(NamespaceRelationId, schemaOid); foreach(lc, requiredExtensions) { @@ -1932,6 +1933,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner, ObjectAddressSet(otherext, ExtensionRelationId, reqext); add_exact_object_address(&otherext, refobjs); + LockNotPinnedObject(ExtensionRelationId, reqext); } /* Record all of them (this includes duplicate elimination) */ @@ -2968,6 +2970,7 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o table_close(extRel, RowExclusiveLock); /* update dependency to point to the new schema */ + LockNotPinnedObject(NamespaceRelationId, nspOid); if (changeDependencyFor(ExtensionRelationId, extensionOid, NamespaceRelationId, oldNspOid, nspOid) != 1) elog(ERROR, "could not change schema dependency for extension %s", @@ -3258,6 +3261,7 @@ ApplyExtensionUpdates(Oid extensionOid, otherext.objectId = reqext; otherext.objectSubId = 0; + LockNotPinnedObject(ExtensionRelationId, reqext); recordDependencyOn(&myself, &otherext, DEPENDENCY_NORMAL); } @@ -3414,6 +3418,7 @@ ExecAlterExtensionContentsRecurse(AlterExtensionContentsStmt *stmt, /* * OK, add the dependency. */ + LockNotPinnedObject(extension.classId, extension.objectId); recordDependencyOn(&object, &extension, DEPENDENCY_EXTENSION); /* diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index cf61bbac1f..735bca486c 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -642,6 +642,7 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwhandler; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwhandler); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -650,6 +651,7 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwvalidator; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwvalidator); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -811,6 +813,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwhandler; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwhandler); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -819,6 +822,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwvalidator; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwvalidator); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } } @@ -951,6 +955,7 @@ CreateForeignServer(CreateForeignServerStmt *stmt) referenced.classId = ForeignDataWrapperRelationId; referenced.objectId = fdw->fdwid; referenced.objectSubId = 0; + LockNotPinnedObject(ForeignDataWrapperRelationId, fdw->fdwid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); recordDependencyOnOwner(ForeignServerRelationId, srvId, ownerId); @@ -1195,6 +1200,7 @@ CreateUserMapping(CreateUserMappingStmt *stmt) referenced.classId = ForeignServerRelationId; referenced.objectId = srv->serverid; referenced.objectSubId = 0; + LockNotPinnedObject(ForeignServerRelationId, srv->serverid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); if (OidIsValid(useId)) @@ -1472,6 +1478,7 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid) referenced.classId = ForeignServerRelationId; referenced.objectId = server->serverid; referenced.objectSubId = 0; + LockNotPinnedObject(ForeignServerRelationId, server->serverid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); table_close(ftrel, RowExclusiveLock); diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c index 6593fd7d81..8207ef08b3 100644 --- a/src/backend/commands/functioncmds.c +++ b/src/backend/commands/functioncmds.c @@ -1446,6 +1446,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) /* Add or replace dependency on support function */ if (OidIsValid(procForm->prosupport)) { + LockNotPinnedObject(ProcedureRelationId, newsupport); if (changeDependencyFor(ProcedureRelationId, funcOid, ProcedureRelationId, procForm->prosupport, newsupport) != 1) @@ -1459,6 +1460,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = newsupport; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, newsupport); recordDependencyOn(&address, &referenced, DEPENDENCY_NORMAL); } @@ -1962,21 +1964,25 @@ CreateTransform(CreateTransformStmt *stmt) /* dependency on language */ ObjectAddressSet(referenced, LanguageRelationId, langid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(LanguageRelationId, langid); /* dependency on type */ ObjectAddressSet(referenced, TypeRelationId, typeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, typeid); /* dependencies on functions */ if (OidIsValid(fromsqlfuncid)) { ObjectAddressSet(referenced, ProcedureRelationId, fromsqlfuncid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, fromsqlfuncid); } if (OidIsValid(tosqlfuncid)) { ObjectAddressSet(referenced, ProcedureRelationId, tosqlfuncid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, tosqlfuncid); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 309389e20d..b14eaad7a3 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -4377,8 +4377,10 @@ IndexSetParentIndex(Relation partitionIdx, Oid parentOid) ObjectAddressSet(parentIdx, RelationRelationId, parentOid); ObjectAddressSet(partitionTbl, RelationRelationId, partitionIdx->rd_index->indrelid); + /* Do we lock for RelationRelationId?? */ recordDependencyOn(&partIdx, &parentIdx, DEPENDENCY_PARTITION_PRI); + /* Do we lock for RelationRelationId?? */ recordDependencyOn(&partIdx, &partitionTbl, DEPENDENCY_PARTITION_SEC); } diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c index b8b5c147c5..e70afd216c 100644 --- a/src/backend/commands/opclasscmds.c +++ b/src/backend/commands/opclasscmds.c @@ -298,12 +298,14 @@ CreateOpFamily(CreateOpFamilyStmt *stmt, const char *opfname, referenced.classId = AccessMethodRelationId; referenced.objectId = amoid; referenced.objectSubId = 0; + LockNotPinnedObject(AccessMethodRelationId, amoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* dependency on namespace */ referenced.classId = NamespaceRelationId; referenced.objectId = namespaceoid; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, namespaceoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* dependency on owner */ @@ -725,18 +727,21 @@ DefineOpClass(CreateOpClassStmt *stmt) referenced.classId = NamespaceRelationId; referenced.objectId = namespaceoid; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, namespaceoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* dependency on opfamily */ referenced.classId = OperatorFamilyRelationId; referenced.objectId = opfamilyoid; referenced.objectSubId = 0; + LockNotPinnedObject(OperatorFamilyRelationId, opfamilyoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* dependency on indexed datatype */ referenced.classId = TypeRelationId; referenced.objectId = typeoid; referenced.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, typeoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* dependency on storage datatype */ @@ -745,6 +750,7 @@ DefineOpClass(CreateOpClassStmt *stmt) referenced.classId = TypeRelationId; referenced.objectId = storageoid; referenced.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, storageoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -1486,6 +1492,13 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, heap_freetuple(tup); + /* + * CommandCounterIncrement() here to ensure the new operator entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + /* Make its dependencies */ myself.classId = AccessMethodOperatorRelationId; myself.objectId = entryoid; @@ -1496,6 +1509,7 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectSubId = 0; /* see comments in amapi.h about dependency strength */ + LockNotPinnedObject(OperatorRelationId, op->object); recordDependencyOn(&myself, &referenced, op->ref_is_hard ? DEPENDENCY_NORMAL : DEPENDENCY_AUTO); @@ -1504,6 +1518,7 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectId = op->refobjid; referenced.objectSubId = 0; + LockNotPinnedObject(referenced.classId, op->refobjid); recordDependencyOn(&myself, &referenced, op->ref_is_hard ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); @@ -1514,6 +1529,7 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectId = op->sortfamily; referenced.objectSubId = 0; + LockNotPinnedObject(OperatorFamilyRelationId, op->sortfamily); recordDependencyOn(&myself, &referenced, op->ref_is_hard ? DEPENDENCY_NORMAL : DEPENDENCY_AUTO); } @@ -1597,6 +1613,7 @@ storeProcedures(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectSubId = 0; /* see comments in amapi.h about dependency strength */ + LockNotPinnedObject(ProcedureRelationId, proc->object); recordDependencyOn(&myself, &referenced, proc->ref_is_hard ? DEPENDENCY_NORMAL : DEPENDENCY_AUTO); @@ -1605,6 +1622,7 @@ storeProcedures(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectId = proc->refobjid; referenced.objectSubId = 0; + LockNotPinnedObject(referenced.classId, proc->refobjid); recordDependencyOn(&myself, &referenced, proc->ref_is_hard ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); diff --git a/src/backend/commands/operatorcmds.c b/src/backend/commands/operatorcmds.c index 5872a3e192..58a69e7cc2 100644 --- a/src/backend/commands/operatorcmds.c +++ b/src/backend/commands/operatorcmds.c @@ -33,6 +33,7 @@ #include "access/htup_details.h" #include "access/table.h" +#include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/pg_namespace.h" @@ -656,11 +657,15 @@ AlterOperator(AlterOperatorStmt *stmt) { replaces[Anum_pg_operator_oprrest - 1] = true; values[Anum_pg_operator_oprrest - 1] = ObjectIdGetDatum(restrictionOid); + if (OidIsValid(restrictionOid)) + LockNotPinnedObject(ProcedureRelationId, restrictionOid); } if (updateJoin) { replaces[Anum_pg_operator_oprjoin - 1] = true; values[Anum_pg_operator_oprjoin - 1] = ObjectIdGetDatum(joinOid); + if (OidIsValid(joinOid)) + LockNotPinnedObject(ProcedureRelationId, joinOid); } if (OidIsValid(commutatorOid)) { @@ -688,6 +693,31 @@ AlterOperator(AlterOperatorStmt *stmt) CatalogTupleUpdate(catalog, &tup->t_self, tup); + + /* Lock dependent objects */ + oprForm = (Form_pg_operator) GETSTRUCT(tup); + + if (OidIsValid(oprForm->oprnamespace)) + LockNotPinnedObject(NamespaceRelationId, oprForm->oprnamespace); + + if (OidIsValid(oprForm->oprleft)) + LockNotPinnedObject(TypeRelationId, oprForm->oprleft); + + if (OidIsValid(oprForm->oprright)) + LockNotPinnedObject(TypeRelationId, oprForm->oprright); + + if (OidIsValid(oprForm->oprresult)) + LockNotPinnedObject(TypeRelationId, oprForm->oprresult); + + if (OidIsValid(oprForm->oprcode)) + LockNotPinnedObject(ProcedureRelationId, oprForm->oprcode); + + if (OidIsValid(oprForm->oprrest)) + LockNotPinnedObject(ProcedureRelationId, oprForm->oprrest); + + if (OidIsValid(oprForm->oprjoin)) + LockNotPinnedObject(ProcedureRelationId, oprForm->oprjoin); + address = makeOperatorDependencies(tup, false, true); if (OidIsValid(commutatorOid) || OidIsValid(negatorOid)) diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c index 6ff3eba824..f04e87ee5d 100644 --- a/src/backend/commands/policy.c +++ b/src/backend/commands/policy.c @@ -722,6 +722,10 @@ CreatePolicy(CreatePolicyStmt *stmt) myself.objectId = policy_id; myself.objectSubId = 0; + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) on + * table_id as it is already locked. + */ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); recordDependencyOnExpr(&myself, qual, qual_pstate->p_rtable, @@ -1053,6 +1057,10 @@ AlterPolicy(AlterPolicyStmt *stmt) myself.objectId = policy_id; myself.objectSubId = 0; + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) on + * table_id as it is already locked. + */ recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); recordDependencyOnExpr(&myself, qual, qual_parse_rtable, DEPENDENCY_NORMAL); diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c index 881f90017e..fadfd9064f 100644 --- a/src/backend/commands/proclang.c +++ b/src/backend/commands/proclang.c @@ -190,12 +190,14 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) /* dependency on the PL handler function */ ObjectAddressSet(referenced, ProcedureRelationId, handlerOid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, handlerOid); /* dependency on the inline handler function, if any */ if (OidIsValid(inlineOid)) { ObjectAddressSet(referenced, ProcedureRelationId, inlineOid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, inlineOid); } /* dependency on the validator function, if any */ @@ -203,6 +205,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) { ObjectAddressSet(referenced, ProcedureRelationId, valOid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, valOid); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c index 28f8522264..80c23d98a5 100644 --- a/src/backend/commands/sequence.c +++ b/src/backend/commands/sequence.c @@ -1681,6 +1681,11 @@ process_owned_by(Relation seqrel, List *owned_by, bool for_identity) depobject.classId = RelationRelationId; depobject.objectId = RelationGetRelid(seqrel); depobject.objectSubId = 0; + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on tablerel as it is already locked. + */ recordDependencyOn(&depobject, &refobject, deptype); } diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index 1db3ef69d2..23a34aeb79 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -536,6 +536,11 @@ CreateStatistics(CreateStatsStmt *stmt) for (i = 0; i < nattnums; i++) { ObjectAddressSubSet(parentobject, RelationRelationId, relid, attnums[i]); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on relid as it is already locked. + */ recordDependencyOn(&myself, &parentobject, DEPENDENCY_AUTO); } @@ -553,6 +558,11 @@ CreateStatistics(CreateStatsStmt *stmt) if (!nattnums) { ObjectAddressSet(parentobject, RelationRelationId, relid); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on relid as it is already locked. + */ recordDependencyOn(&myself, &parentobject, DEPENDENCY_AUTO); } @@ -573,6 +583,7 @@ CreateStatistics(CreateStatsStmt *stmt) * than the underlying table(s). */ ObjectAddressSet(parentobject, NamespaceRelationId, namespaceId); + LockNotPinnedObject(NamespaceRelationId, namespaceId); recordDependencyOn(&myself, &parentobject, DEPENDENCY_NORMAL); recordDependencyOnOwner(StatisticExtRelationId, statoid, stxowner); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 66cda26a25..4b5fcca399 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -3438,6 +3438,10 @@ StoreCatalogInheritance1(Oid relationId, Oid parentOid, childobject.objectId = relationId; childobject.objectSubId = 0; + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) on + * parentOid as it is already locked. + */ recordDependencyOn(&childobject, &parentobject, child_dependency_type(child_is_partition)); @@ -7349,7 +7353,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, /* * Add needed dependency entries for the new column. */ + LockNotPinnedObject(TypeRelationId, attribute->atttypid); add_column_datatype_dependency(myrelid, newattnum, attribute->atttypid); + LockNotPinnedObject(CollationRelationId, attribute->attcollation); add_column_collation_dependency(myrelid, newattnum, attribute->attcollation); /* @@ -10174,6 +10180,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, ObjectAddress referenced; ObjectAddressSet(referenced, ConstraintRelationId, parentConstr); + LockNotPinnedObject(ConstraintRelationId, parentConstr); recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL); } @@ -10465,8 +10472,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, */ ObjectAddressSet(address, ConstraintRelationId, constrOid); ObjectAddressSet(referenced, ConstraintRelationId, parentConstr); + LockNotPinnedObject(ConstraintRelationId, parentConstr); recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, partitionId); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on partitionId as it is already locked. + */ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC); /* Make all this visible before recursing */ @@ -10967,9 +10980,15 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) /* Set up partition dependencies for the new constraint */ ObjectAddressSet(address, ConstraintRelationId, constrOid); ObjectAddressSet(referenced, ConstraintRelationId, parentConstrOid); + LockDatabaseObject(ConstraintRelationId, parentConstrOid, 0, AccessShareLock); recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(partRel)); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on partRel as it is already locked. + */ recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC); /* Done with the cloned constraint's tuple */ @@ -13254,7 +13273,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, table_close(attrelation, RowExclusiveLock); /* Install dependencies on new datatype and collation */ + LockNotPinnedObject(TypeRelationId, targettype); add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); + LockNotPinnedObject(CollationRelationId, targetcollid); add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); /* @@ -14816,6 +14837,7 @@ ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) */ ObjectAddressSet(relobj, RelationRelationId, reloid); ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam); + LockNotPinnedObject(AccessMethodRelationId, rd_rel->relam); recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL); } else if (OidIsValid(oldAccessMethodId) && @@ -14835,6 +14857,7 @@ ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) OidIsValid(rd_rel->relam)); /* Both are valid, so update the dependency */ + LockNotPinnedObject(AccessMethodRelationId, rd_rel->relam); changeDependencyFor(RelationRelationId, reloid, AccessMethodRelationId, oldAccessMethodId, rd_rel->relam); @@ -16434,6 +16457,7 @@ ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode) typeobj.classId = TypeRelationId; typeobj.objectId = typeid; typeobj.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, typeid); recordDependencyOn(&tableobj, &typeobj, DEPENDENCY_NORMAL); /* Update pg_class.reloftype */ @@ -17192,14 +17216,17 @@ AlterRelationNamespaceInternal(Relation classRel, Oid relOid, CatalogTupleUpdate(classRel, &classTup->t_self, classTup); /* Update dependency on schema if caller said so */ - if (hasDependEntry && - changeDependencyFor(RelationRelationId, - relOid, - NamespaceRelationId, - oldNspOid, - newNspOid) != 1) - elog(ERROR, "could not change schema dependency for relation \"%s\"", - NameStr(classForm->relname)); + if (hasDependEntry) + { + LockNotPinnedObject(NamespaceRelationId, newNspOid); + if (changeDependencyFor(RelationRelationId, + relOid, + NamespaceRelationId, + oldNspOid, + newNspOid) != 1) + elog(ERROR, "could not change schema dependency for relation \"%s\"", + NameStr(classForm->relname)); + } } if (!already_done) { diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 95de402fa6..99433cce75 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -1018,8 +1018,6 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, ((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true; CatalogTupleUpdate(pgrel, &tuple->t_self, tuple); - - CommandCounterIncrement(); } else CacheInvalidateRelcacheByTuple(tuple); @@ -1027,6 +1025,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, heap_freetuple(tuple); table_close(pgrel, RowExclusiveLock); + /* + * CommandCounterIncrement() here to ensure the new trigger entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + /* * If we're replacing a trigger, flush all the old dependencies before * recording new ones. @@ -1045,6 +1050,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = ProcedureRelationId; referenced.objectId = funcoid; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, funcoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); if (isInternal && OidIsValid(constraintOid)) @@ -1058,6 +1064,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = ConstraintRelationId; referenced.objectId = constraintOid; referenced.objectSubId = 0; + LockNotPinnedObject(ConstraintRelationId, constraintOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); } else @@ -1070,6 +1077,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = RelationRelationId; referenced.objectId = RelationGetRelid(rel); referenced.objectSubId = 0; + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on rel as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); if (OidIsValid(constrrelid)) @@ -1077,6 +1089,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = RelationRelationId; referenced.objectId = constrrelid; referenced.objectSubId = 0; + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on constrrelid as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); } /* Not possible to have an index dependency in this case */ @@ -1091,6 +1108,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = ConstraintRelationId; referenced.objectId = constraintOid; referenced.objectSubId = 0; + LockNotPinnedObject(TriggerRelationId, trigoid); recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL); } @@ -1100,8 +1118,14 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, if (OidIsValid(parentTriggerOid)) { ObjectAddressSet(referenced, TriggerRelationId, parentTriggerOid); + LockNotPinnedObject(TriggerRelationId, parentTriggerOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel)); + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on rel as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC); } } @@ -1116,6 +1140,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, for (i = 0; i < ncolumns; i++) { referenced.objectSubId = columns[i]; + + /* + * No need to call LockRelationOid() (through + * LockNotPinnedObject()) on rel as it is already locked. + */ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } } @@ -1255,9 +1284,15 @@ TriggerSetParentTrigger(Relation trigRel, ObjectAddressSet(depender, TriggerRelationId, childTrigId); ObjectAddressSet(referenced, TriggerRelationId, parentTrigId); + LockNotPinnedObject(TriggerRelationId, parentTrigId); recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, childTableId); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on childTableId as it is already locked. + */ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC); } else diff --git a/src/backend/commands/tsearchcmds.c b/src/backend/commands/tsearchcmds.c index b7b5019f1e..29e02f4946 100644 --- a/src/backend/commands/tsearchcmds.c +++ b/src/backend/commands/tsearchcmds.c @@ -66,12 +66,12 @@ static DefElem *buildDefItem(const char *name, const char *val, /* --------------------- TS Parser commands ------------------------ */ /* - * lookup a parser support function and return its OID (as a Datum) + * lookup a parser support function and return its OID * * attnum is the pg_ts_parser column the function will go into */ -static Datum -get_ts_parser_func(DefElem *defel, int attnum) +static Oid +get_ts_parser_func_oid(DefElem *defel, int attnum) { List *funcName = defGetQualifiedName(defel); Oid typeId[3]; @@ -125,7 +125,7 @@ get_ts_parser_func(DefElem *defel, int attnum) func_signature_string(funcName, nargs, NIL, typeId), format_type_be(retTypeId)))); - return ObjectIdGetDatum(procOid); + return procOid; } /* @@ -214,6 +214,7 @@ DefineTSParser(List *names, List *parameters) namestrcpy(&pname, prsname); values[Anum_pg_ts_parser_prsname - 1] = NameGetDatum(&pname); values[Anum_pg_ts_parser_prsnamespace - 1] = ObjectIdGetDatum(namespaceoid); + LockNotPinnedObject(NamespaceRelationId, namespaceoid); /* * loop over the definition list and extract the information we need. @@ -224,28 +225,38 @@ DefineTSParser(List *names, List *parameters) if (strcmp(defel->defname, "start") == 0) { - values[Anum_pg_ts_parser_prsstart - 1] = - get_ts_parser_func(defel, Anum_pg_ts_parser_prsstart); + Oid procoid = get_ts_parser_func_oid(defel, Anum_pg_ts_parser_prsstart); + + values[Anum_pg_ts_parser_prsstart - 1] = ObjectIdGetDatum(procoid); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "gettoken") == 0) { - values[Anum_pg_ts_parser_prstoken - 1] = - get_ts_parser_func(defel, Anum_pg_ts_parser_prstoken); + Oid procoid = get_ts_parser_func_oid(defel, Anum_pg_ts_parser_prstoken); + + values[Anum_pg_ts_parser_prstoken - 1] = ObjectIdGetDatum(procoid); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "end") == 0) { - values[Anum_pg_ts_parser_prsend - 1] = - get_ts_parser_func(defel, Anum_pg_ts_parser_prsend); + Oid procoid = get_ts_parser_func_oid(defel, Anum_pg_ts_parser_prsend); + + values[Anum_pg_ts_parser_prsend - 1] = ObjectIdGetDatum(procoid); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "headline") == 0) { - values[Anum_pg_ts_parser_prsheadline - 1] = - get_ts_parser_func(defel, Anum_pg_ts_parser_prsheadline); + Oid procoid = get_ts_parser_func_oid(defel, Anum_pg_ts_parser_prsheadline); + + values[Anum_pg_ts_parser_prsheadline - 1] = ObjectIdGetDatum(procoid); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "lextypes") == 0) { - values[Anum_pg_ts_parser_prslextype - 1] = - get_ts_parser_func(defel, Anum_pg_ts_parser_prslextype); + Oid procoid = get_ts_parser_func_oid(defel, Anum_pg_ts_parser_prslextype); + + values[Anum_pg_ts_parser_prslextype - 1] = ObjectIdGetDatum(procoid); + LockNotPinnedObject(ProcedureRelationId, procoid); } else ereport(ERROR, @@ -474,6 +485,10 @@ DefineTSDictionary(List *names, List *parameters) CatalogTupleInsert(dictRel, tup); + /* Lock objects */ + LockNotPinnedObject(NamespaceRelationId, namespaceoid); + LockNotPinnedObject(TSTemplateRelationId, templId); + address = makeDictionaryDependencies(tup); /* Post creation hook for new text search dictionary */ @@ -601,12 +616,12 @@ AlterTSDictionary(AlterTSDictionaryStmt *stmt) /* ---------------------- TS Template commands -----------------------*/ /* - * lookup a template support function and return its OID (as a Datum) + * lookup a template support function and return its OID * * attnum is the pg_ts_template column the function will go into */ -static Datum -get_ts_template_func(DefElem *defel, int attnum) +static Oid +get_ts_template_func_oid(DefElem *defel, int attnum) { List *funcName = defGetQualifiedName(defel); Oid typeId[4]; @@ -642,7 +657,7 @@ get_ts_template_func(DefElem *defel, int attnum) func_signature_string(funcName, nargs, NIL, typeId), format_type_be(retTypeId)))); - return ObjectIdGetDatum(procOid); + return procOid; } /* @@ -723,6 +738,7 @@ DefineTSTemplate(List *names, List *parameters) namestrcpy(&dname, tmplname); values[Anum_pg_ts_template_tmplname - 1] = NameGetDatum(&dname); values[Anum_pg_ts_template_tmplnamespace - 1] = ObjectIdGetDatum(namespaceoid); + LockNotPinnedObject(NamespaceRelationId, namespaceoid); /* * loop over the definition list and extract the information we need. @@ -733,15 +749,19 @@ DefineTSTemplate(List *names, List *parameters) if (strcmp(defel->defname, "init") == 0) { - values[Anum_pg_ts_template_tmplinit - 1] = - get_ts_template_func(defel, Anum_pg_ts_template_tmplinit); + Oid procoid = get_ts_template_func_oid(defel, Anum_pg_ts_template_tmplinit); + + values[Anum_pg_ts_template_tmplinit - 1] = ObjectIdGetDatum(procoid); nulls[Anum_pg_ts_template_tmplinit - 1] = false; + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "lexize") == 0) { - values[Anum_pg_ts_template_tmpllexize - 1] = - get_ts_template_func(defel, Anum_pg_ts_template_tmpllexize); + Oid procoid = get_ts_template_func_oid(defel, Anum_pg_ts_template_tmpllexize); + + values[Anum_pg_ts_template_tmpllexize - 1] = ObjectIdGetDatum(procoid); nulls[Anum_pg_ts_template_tmpllexize - 1] = false; + LockNotPinnedObject(ProcedureRelationId, procoid); } else ereport(ERROR, @@ -879,6 +899,7 @@ makeConfigurationDependencies(HeapTuple tuple, bool removeOld, referenced.objectId = cfgmap->mapdict; referenced.objectSubId = 0; add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TSDictionaryRelationId, cfgmap->mapdict); } systable_endscan(scan); @@ -998,6 +1019,10 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied) values[Anum_pg_ts_config_cfgowner - 1] = ObjectIdGetDatum(GetUserId()); values[Anum_pg_ts_config_cfgparser - 1] = ObjectIdGetDatum(prsOid); + /* Lock objects */ + LockNotPinnedObject(NamespaceRelationId, namespaceoid); + LockNotPinnedObject(TSParserRelationId, prsOid); + tup = heap_form_tuple(cfgRel->rd_att, values, nulls); CatalogTupleInsert(cfgRel, tup); @@ -1156,6 +1181,7 @@ ObjectAddress AlterTSConfiguration(AlterTSConfigurationStmt *stmt) { HeapTuple tup; + Form_pg_ts_config cfg; Oid cfgId; Relation relMap; ObjectAddress address; @@ -1168,7 +1194,8 @@ AlterTSConfiguration(AlterTSConfigurationStmt *stmt) errmsg("text search configuration \"%s\" does not exist", NameListToString(stmt->cfgname)))); - cfgId = ((Form_pg_ts_config) GETSTRUCT(tup))->oid; + cfg = (Form_pg_ts_config) GETSTRUCT(tup); + cfgId = cfg->oid; /* must be owner */ if (!object_ownercheck(TSConfigRelationId, cfgId, GetUserId())) @@ -1183,6 +1210,10 @@ AlterTSConfiguration(AlterTSConfigurationStmt *stmt) else if (stmt->tokentype) DropConfigurationMapping(stmt, tup, relMap); + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, cfg->cfgnamespace); + LockNotPinnedObject(TSParserRelationId, cfg->cfgparser); + /* Update dependencies */ makeConfigurationDependencies(tup, true, relMap); @@ -1414,6 +1445,8 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt, repl_val[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictNew); repl_repl[Anum_pg_ts_config_map_mapdict - 1] = true; + LockNotPinnedObject(TSDictionaryRelationId, dictNew); + newtup = heap_modify_tuple(maptup, RelationGetDescr(relMap), repl_val, repl_null, repl_repl); @@ -1456,6 +1489,8 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt, slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = Int32GetDatum(j + 1); slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictIds[j]); + LockNotPinnedObject(TSDictionaryRelationId, dictIds[j]); + ExecStoreVirtualTuple(slot[slotCount]); slotCount++; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 2a1e713335..9febaa24a7 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1794,6 +1794,7 @@ makeRangeConstructors(const char *name, Oid namespace, * that they go away silently when the type is dropped. Note that * pg_dump depends on this choice to avoid dumping the constructors. */ + LockNotPinnedObject(TypeRelationId, rangeOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); } } @@ -1859,6 +1860,7 @@ makeMultirangeConstructors(const char *name, Oid namespace, * that they go away silently when the type is dropped. Note that pg_dump * depends on this choice to avoid dumping the constructors. */ + LockNotPinnedObject(TypeRelationId, multirangeOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); pfree(argtypes); @@ -2672,6 +2674,45 @@ AlterDomainDefault(List *names, Node *defaultRaw) CatalogTupleUpdate(rel, &tup->t_self, newtuple); + /* Lock dependent objects */ + typTup = (Form_pg_type) GETSTRUCT(newtuple); + + if (OidIsValid(typTup->typnamespace)) + LockNotPinnedObject(NamespaceRelationId, typTup->typnamespace); + + if (OidIsValid(typTup->typinput)) + LockNotPinnedObject(ProcedureRelationId, typTup->typinput); + + if (OidIsValid(typTup->typoutput)) + LockNotPinnedObject(ProcedureRelationId, typTup->typoutput); + + if (OidIsValid(typTup->typreceive)) + LockNotPinnedObject(ProcedureRelationId, typTup->typreceive); + + if (OidIsValid(typTup->typsend)) + LockNotPinnedObject(ProcedureRelationId, typTup->typsend); + + if (OidIsValid(typTup->typmodin)) + LockNotPinnedObject(ProcedureRelationId, typTup->typmodin); + + if (OidIsValid(typTup->typmodout)) + LockNotPinnedObject(ProcedureRelationId, typTup->typmodout); + + if (OidIsValid(typTup->typanalyze)) + LockNotPinnedObject(ProcedureRelationId, typTup->typanalyze); + + if (OidIsValid(typTup->typsubscript)) + LockNotPinnedObject(ProcedureRelationId, typTup->typsubscript); + + if (OidIsValid(typTup->typbasetype)) + LockNotPinnedObject(TypeRelationId, typTup->typbasetype); + + if (OidIsValid(typTup->typcollation)) + LockNotPinnedObject(CollationRelationId, typTup->typcollation); + + if (OidIsValid(typTup->typelem)) + LockNotPinnedObject(TypeRelationId, typTup->typelem); + /* Rebuild dependencies */ GenerateTypeDependencies(newtuple, rel, @@ -4276,10 +4317,13 @@ AlterTypeNamespaceInternal(Oid typeOid, Oid nspOid, if (oldNspOid != nspOid && (isCompositeType || typform->typtype != TYPTYPE_COMPOSITE) && !isImplicitArray) + { + LockNotPinnedObject(NamespaceRelationId, nspOid); if (changeDependencyFor(TypeRelationId, typeOid, NamespaceRelationId, oldNspOid, nspOid) != 1) elog(ERROR, "could not change schema dependency for type \"%s\"", format_type_be(typeOid)); + } InvokeObjectPostAlterHook(TypeRelationId, typeOid, 0); @@ -4571,6 +4615,7 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray, SysScanDesc scan; ScanKeyData key[1]; HeapTuple domainTup; + Form_pg_type typeForm; /* Since this function recurses, it could be driven to stack overflow */ check_stack_depth(); @@ -4619,6 +4664,45 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray, newtup = heap_modify_tuple(tup, RelationGetDescr(catalog), values, nulls, replaces); + /* Lock dependent objects */ + typeForm = (Form_pg_type) GETSTRUCT(newtup); + + if (OidIsValid(typeForm->typnamespace)) + LockNotPinnedObject(NamespaceRelationId, typeForm->typnamespace); + + if (OidIsValid(typeForm->typinput)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typinput); + + if (OidIsValid(typeForm->typoutput)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typoutput); + + if (OidIsValid(typeForm->typreceive)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typreceive); + + if (OidIsValid(typeForm->typsend)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typsend); + + if (OidIsValid(typeForm->typmodin)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typmodin); + + if (OidIsValid(typeForm->typmodout)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typmodout); + + if (OidIsValid(typeForm->typanalyze)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typanalyze); + + if (OidIsValid(typeForm->typsubscript)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typsubscript); + + if (OidIsValid(typeForm->typbasetype)) + LockNotPinnedObject(TypeRelationId, typeForm->typbasetype); + + if (OidIsValid(typeForm->typcollation)) + LockNotPinnedObject(CollationRelationId, typeForm->typcollation); + + if (OidIsValid(typeForm->typelem)) + LockNotPinnedObject(TypeRelationId, typeForm->typelem); + CatalogTupleUpdate(catalog, &newtup->t_self, newtup); /* Rebuild dependencies for this type */ diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 6cc9a8d8bf..06f6c952e3 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -155,6 +155,10 @@ InsertRule(const char *rulname, referenced.objectId = eventrel_oid; referenced.objectSubId = 0; + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) on + * eventrel_oid as it is already locked. + */ recordDependencyOn(&myself, &referenced, (evtype == CMD_SELECT) ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index 3250d539e1..60e8539fe3 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -271,6 +271,7 @@ Section: Class 28 - Invalid Authorization Specification Section: Class 2B - Dependent Privilege Descriptors Still Exist 2B000 E ERRCODE_DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST dependent_privilege_descriptors_still_exist +2BP02 E ERRCODE_DEPENDENT_OBJECTS_DOES_NOT_EXIST dependent_objects_does_not_exist 2BP01 E ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST dependent_objects_still_exist Section: Class 2D - Invalid Transaction Termination diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 7eee66f810..c57204cc40 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -101,6 +101,9 @@ typedef struct ObjectAddresses ObjectAddresses; /* in dependency.c */ extern void AcquireDeletionLock(const ObjectAddress *object, int flags); +extern void LockNotPinnedObjectById(const ObjectAddress *object); +extern void LockNotPinnedObjectsById(const ObjectAddress *object, int nobject); +extern void LockNotPinnedObject(Oid classid, Oid objid); extern void ReleaseDeletionLock(const ObjectAddress *object); @@ -128,6 +131,9 @@ extern void add_exact_object_address(const ObjectAddress *object, extern bool object_address_present(const ObjectAddress *object, const ObjectAddresses *addrs); +extern void lock_record_object_address_dependencies(const ObjectAddress *depender, + ObjectAddresses *referenced, + DependencyType behavior); extern void record_object_address_dependencies(const ObjectAddress *depender, ObjectAddresses *referenced, DependencyType behavior); @@ -172,6 +178,7 @@ extern long changeDependenciesOf(Oid classId, Oid oldObjectId, extern long changeDependenciesOn(Oid refClassId, Oid oldRefObjectId, Oid newRefObjectId); +extern bool isObjectPinned(const ObjectAddress *object); extern Oid getExtensionOfObject(Oid classId, Oid objectId); extern List *getAutoExtensionsOfObject(Oid classId, Oid objectId); diff --git a/src/include/catalog/objectaddress.h b/src/include/catalog/objectaddress.h index 3a70d80e32..56f746264b 100644 --- a/src/include/catalog/objectaddress.h +++ b/src/include/catalog/objectaddress.h @@ -53,6 +53,7 @@ extern void check_object_ownership(Oid roleid, Node *object, Relation relation); extern Oid get_object_namespace(const ObjectAddress *address); +extern bool ObjectByIdExist(const ObjectAddress *address); extern bool is_objectclass_supported(Oid class_id); extern const char *get_object_class_descr(Oid class_id); diff --git a/src/test/isolation/expected/test_dependencies_locks.out b/src/test/isolation/expected/test_dependencies_locks.out new file mode 100644 index 0000000000..9b645d7aa5 --- /dev/null +++ b/src/test/isolation/expected/test_dependencies_locks.out @@ -0,0 +1,129 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_begin s1_create_function_in_schema s2_drop_schema s1_commit +step s1_begin: BEGIN; +step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; +step s2_drop_schema: DROP SCHEMA testschema; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_schema: <... completed> +ERROR: cannot drop schema testschema because other objects depend on it + +starting permutation: s2_begin s2_drop_schema s1_create_function_in_schema s2_commit +step s2_begin: BEGIN; +step s2_drop_schema: DROP SCHEMA testschema; +step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_in_schema: <... completed> +ERROR: schema testschema does not exist + +starting permutation: s1_begin s1_alter_function_schema s2_drop_alterschema s1_commit +step s1_begin: BEGIN; +step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; +step s2_drop_alterschema: DROP SCHEMA alterschema; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_alterschema: <... completed> +ERROR: cannot drop schema alterschema because other objects depend on it + +starting permutation: s2_begin s2_drop_alterschema s1_alter_function_schema s2_commit +step s2_begin: BEGIN; +step s2_drop_alterschema: DROP SCHEMA alterschema; +step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; <waiting ...> +step s2_commit: COMMIT; +step s1_alter_function_schema: <... completed> +ERROR: schema alterschema does not exist + +starting permutation: s1_begin s1_create_function_with_argtype s2_drop_foo_type s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; +step s2_drop_foo_type: DROP TYPE public.foo; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_foo_type: <... completed> +ERROR: cannot drop type foo because other objects depend on it + +starting permutation: s2_begin s2_drop_foo_type s1_create_function_with_argtype s2_commit +step s2_begin: BEGIN; +step s2_drop_foo_type: DROP TYPE public.foo; +step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_with_argtype: <... completed> +ERROR: type foo does not exist + +starting permutation: s1_begin s1_create_function_with_rettype s2_drop_foo_rettype s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; +step s2_drop_foo_rettype: DROP DOMAIN id; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_foo_rettype: <... completed> +ERROR: cannot drop type id because other objects depend on it + +starting permutation: s2_begin s2_drop_foo_rettype s1_create_function_with_rettype s2_commit +step s2_begin: BEGIN; +step s2_drop_foo_rettype: DROP DOMAIN id; +step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_with_rettype: <... completed> +ERROR: type id does not exist + +starting permutation: s1_begin s1_create_function_with_function s2_drop_function_f s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; +step s2_drop_function_f: DROP FUNCTION f(); <waiting ...> +step s1_commit: COMMIT; +step s2_drop_function_f: <... completed> +ERROR: cannot drop function f() because other objects depend on it + +starting permutation: s2_begin s2_drop_function_f s1_create_function_with_function s2_commit +step s2_begin: BEGIN; +step s2_drop_function_f: DROP FUNCTION f(); +step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_with_function: <... completed> +ERROR: function f() does not exist + +starting permutation: s1_begin s1_create_domain_with_domain s2_drop_domain_id s1_commit +step s1_begin: BEGIN; +step s1_create_domain_with_domain: CREATE DOMAIN idid as id; +step s2_drop_domain_id: DROP DOMAIN id; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_domain_id: <... completed> +ERROR: cannot drop type id because other objects depend on it + +starting permutation: s2_begin s2_drop_domain_id s1_create_domain_with_domain s2_commit +step s2_begin: BEGIN; +step s2_drop_domain_id: DROP DOMAIN id; +step s1_create_domain_with_domain: CREATE DOMAIN idid as id; <waiting ...> +step s2_commit: COMMIT; +step s1_create_domain_with_domain: <... completed> +ERROR: type id does not exist + +starting permutation: s1_begin s1_create_table_with_type s2_drop_footab_type s1_commit +step s1_begin: BEGIN; +step s1_create_table_with_type: CREATE TABLE tabtype(a footab); +step s2_drop_footab_type: DROP TYPE public.footab; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_footab_type: <... completed> +ERROR: cannot drop type footab because other objects depend on it + +starting permutation: s2_begin s2_drop_footab_type s1_create_table_with_type s2_commit +step s2_begin: BEGIN; +step s2_drop_footab_type: DROP TYPE public.footab; +step s1_create_table_with_type: CREATE TABLE tabtype(a footab); <waiting ...> +step s2_commit: COMMIT; +step s1_create_table_with_type: <... completed> +ERROR: type footab does not exist + +starting permutation: s1_begin s1_create_server_with_fdw_wrapper s2_drop_fdw_wrapper s1_commit +step s1_begin: BEGIN; +step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; +step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_fdw_wrapper: <... completed> +ERROR: cannot drop foreign-data wrapper fdw_wrapper because other objects depend on it + +starting permutation: s2_begin s2_drop_fdw_wrapper s1_create_server_with_fdw_wrapper s2_commit +step s2_begin: BEGIN; +step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; +step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; <waiting ...> +step s2_commit: COMMIT; +step s1_create_server_with_fdw_wrapper: <... completed> +ERROR: foreign-data wrapper fdw_wrapper does not exist diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 0342eb39e4..1b67f0bffe 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -114,3 +114,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: test_dependencies_locks diff --git a/src/test/isolation/specs/test_dependencies_locks.spec b/src/test/isolation/specs/test_dependencies_locks.spec new file mode 100644 index 0000000000..5d04dfe9dc --- /dev/null +++ b/src/test/isolation/specs/test_dependencies_locks.spec @@ -0,0 +1,89 @@ +setup +{ + CREATE SCHEMA testschema; + CREATE SCHEMA alterschema; + CREATE TYPE public.foo as enum ('one', 'two'); + CREATE TYPE public.footab as enum ('three', 'four'); + CREATE DOMAIN id AS int; + CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; + CREATE FUNCTION public.falter() RETURNS int LANGUAGE SQL RETURN 1; + CREATE FOREIGN DATA WRAPPER fdw_wrapper; +} + +teardown +{ + DROP FUNCTION IF EXISTS testschema.foo(); + DROP FUNCTION IF EXISTS fooargtype(num foo); + DROP FUNCTION IF EXISTS footrettype(); + DROP FUNCTION IF EXISTS foofunc(); + DROP FUNCTION IF EXISTS public.falter(); + DROP FUNCTION IF EXISTS alterschema.falter(); + DROP DOMAIN IF EXISTS idid; + DROP SERVER IF EXISTS srv_fdw_wrapper; + DROP TABLE IF EXISTS tabtype; + DROP SCHEMA IF EXISTS testschema; + DROP SCHEMA IF EXISTS alterschema; + DROP TYPE IF EXISTS public.foo; + DROP TYPE IF EXISTS public.footab; + DROP DOMAIN IF EXISTS id; + DROP FUNCTION IF EXISTS f(); + DROP FOREIGN DATA WRAPPER IF EXISTS fdw_wrapper; +} + +session "s1" + +step "s1_begin" { BEGIN; } +step "s1_create_function_in_schema" { CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; } +step "s1_create_function_with_argtype" { CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; } +step "s1_create_function_with_rettype" { CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; } +step "s1_create_function_with_function" { CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; } +step "s1_alter_function_schema" { ALTER FUNCTION public.falter() SET SCHEMA alterschema; } +step "s1_create_domain_with_domain" { CREATE DOMAIN idid as id; } +step "s1_create_table_with_type" { CREATE TABLE tabtype(a footab); } +step "s1_create_server_with_fdw_wrapper" { CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; } +step "s1_commit" { COMMIT; } + +session "s2" + +step "s2_begin" { BEGIN; } +step "s2_drop_schema" { DROP SCHEMA testschema; } +step "s2_drop_alterschema" { DROP SCHEMA alterschema; } +step "s2_drop_foo_type" { DROP TYPE public.foo; } +step "s2_drop_foo_rettype" { DROP DOMAIN id; } +step "s2_drop_footab_type" { DROP TYPE public.footab; } +step "s2_drop_function_f" { DROP FUNCTION f(); } +step "s2_drop_domain_id" { DROP DOMAIN id; } +step "s2_drop_fdw_wrapper" { DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; } +step "s2_commit" { COMMIT; } + +# function - schema +permutation "s1_begin" "s1_create_function_in_schema" "s2_drop_schema" "s1_commit" +permutation "s2_begin" "s2_drop_schema" "s1_create_function_in_schema" "s2_commit" + +# alter function - schema +permutation "s1_begin" "s1_alter_function_schema" "s2_drop_alterschema" "s1_commit" +permutation "s2_begin" "s2_drop_alterschema" "s1_alter_function_schema" "s2_commit" + +# function - argtype +permutation "s1_begin" "s1_create_function_with_argtype" "s2_drop_foo_type" "s1_commit" +permutation "s2_begin" "s2_drop_foo_type" "s1_create_function_with_argtype" "s2_commit" + +# function - rettype +permutation "s1_begin" "s1_create_function_with_rettype" "s2_drop_foo_rettype" "s1_commit" +permutation "s2_begin" "s2_drop_foo_rettype" "s1_create_function_with_rettype" "s2_commit" + +# function - function +permutation "s1_begin" "s1_create_function_with_function" "s2_drop_function_f" "s1_commit" +permutation "s2_begin" "s2_drop_function_f" "s1_create_function_with_function" "s2_commit" + +# domain - domain +permutation "s1_begin" "s1_create_domain_with_domain" "s2_drop_domain_id" "s1_commit" +permutation "s2_begin" "s2_drop_domain_id" "s1_create_domain_with_domain" "s2_commit" + +# table - type +permutation "s1_begin" "s1_create_table_with_type" "s2_drop_footab_type" "s1_commit" +permutation "s2_begin" "s2_drop_footab_type" "s1_create_table_with_type" "s2_commit" + +# server - foreign data wrapper +permutation "s1_begin" "s1_create_server_with_fdw_wrapper" "s2_drop_fdw_wrapper" "s1_commit" +permutation "s2_begin" "s2_drop_fdw_wrapper" "s1_create_server_with_fdw_wrapper" "s2_commit" -- 2.34.1 --TZWrRHsroaNOFM1+-- ^ permalink raw reply [nested|flat] 383+ messages in thread
end of thread, other threads:[~2024-03-29 15:43 UTC | newest] Thread overview: 383+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v31 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v34 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v28 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v29 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v29 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v32 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v30 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v38 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v37 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2023-05-31 10:08 [PATCH v33 07/11] Add DISTINCT support for IVM Yugo Nagata <[email protected]> 2024-03-29 15:43 [PATCH v11] Avoid orphaned objects dependencies Bertrand Drouvot <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox